27 February 2016

Spring-boot, MVC, Freemarker, dan Twitter-Bootstrap (bag 6)

Dalam dunia komputer, sebuah aplikasi web memiliki satu standar yang tidak boleh tidak ada di dalamnya, standar tersebut adalah keamanan. Kelengkapan sebuah aplikasi web ditandai oleh ada tidaknya fitur keamanan data dalam aplikasi web itu sendiri. Keamanan data digunakan aplikasi web untuk menjaga data-data dari tangan-tangan jahil yang hendak merusak data. Dengan adanya fitur keamanan dalam sebuah aplikasi web, hanya orang-orang tertentu saja yang bisa mengakses aplikasi web tersebut.
Keamanan data dalam sebuah aplikasi web, biasanya memiliki tingkatan-tingkatan tersendiri, tingkatan-tingkatan ini diwujudkan dalam konsep boleh atau tidaknya seorang user mengakses suatu data. Tingkatan-tingkatan ini disebut dengan istilah ROLE. Role ini yang akan mengatur apakah suatu user boleh mengakses suatu data atau tidak. Sebagai contoh “user dengan role ADMIN bisa mengakses seluruh data, termasuk data master, dan user dengan role USER hanya boleh mengakses data-data transaksi”. Jika dilihat dari sisi kepemilikan, satu user boleh memiliki lebih dari satu role. Sehingga skenario role ini bisa saja menjadi seperti “user dengan role ADMIN boleh mengakses data master maupun data transaksi”. Penentuan role ini sendiri biasanya bergantung dari fungsi dan kegunaan aplikasi web itu. 
Untuk tutorial lanjutan kita kali ini tentang spring-boot, kita akan mencoba membuat aplikasi web yang sudah kita pelajari selama ini memiliki sistem keamanan, sehingga data-data dalam aplikasi web kita bisa terlindung dengan baik seperti layaknya standar sebuah aplikasi web. Kita juga akan mencoba mengimplementasikan tingkatan-tingkatan sistem keamanan di dalamnya. Yang tentunya menggunakan skenario boleh tidaknya suatu user dengan role tertentu mengakses suatu data dalam aplikasi web kita nantinya.
Spring framework memiliki koleksi pustaka (library) yang cukup lengkap untuk membuat sebuah aplikasi web yang kokoh, efisien dan juga efektif, termasuk sistem keamanan. Spring framework juga dilengkapi dengan pustaka keamanan (security libraries) yang cukup handal, dan banyak digunakan di seluruh dunia. Namun demikian library spring security ini memang terpisah dari distribusi spring framework ataupun spring-boot, ini dilakukan agar para pengguna spring framework bisa memilih sistem keamanan yang disukai masing-masing.. Mari kita coba impelementasikan sistem keamanan ini pada aplikasi web kita.
Bundled library yang akan kita gunakan untuk mengimplementasikan sistem keamanan  dalam aplikasi web kita bernama spring-security-framework. Oleh karena library ini terpisah dari distribusi spring-boot, maka kita harus menambahkan sendiri library spring-security tersebut. Tambahkanlah kode-kode berikut ini dalam file pom.xml :
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>1.2.2.RELEASE</version>
</dependency>
Untuk lebih mengenal seperti apa spring-security tersebut jika kita impelementasikan ke dalam aplikasi web kita, maka silahkan langsung jalankan saja aplikasi web kita. Kemudian bukalah browser dan ketiklah dalam browser alamat url http://localhost:8080/. Jika mungkin anda lupa bagaimana cara menjalankan aplikasi web ini, silahkan buka kembali tutorial sebelumnya di sini. Ingat sebelumnya kita sudah pernah membuat mapping untuk url “/” dalam controller IndexController. Apakah setelah kita eksekusi tampilan halaman web aplikasi kita ini seperti gambar berikut ini:
Jika ya, maka aplikasi web kita sudah terlindung dengan sistem keamanan. Lantas apakah kita sudah selesai? Tentu saja belum, ini baru pengenalan awal tentang spring-security. Baiklah mari kita bahas, ketika kita menambahkan library spring-boot-security-starter pada file pom.xml, spring-boot sudah langsung mengenali bahwa aplikasi web yang sedang dijalankan memiliki suatu keamanan data. Karena belum ada satupun konfigurasi yang kita lakukan untuk security ini, maka spring-security akan menganggap seluruh url dilindungi oleh keamanan, sehingga muncul prompt seperti gambar diatas. Lalu bagaimana dengan user dan password yang harus dimasukkan dalam prompt yang muncul pada aplikasi web tersebut?. Secara default spring-security mengenali “user” sebagai username dan password yang dihasilkan secara random dan dicetak di dalam konsol ketika kita menjalankan aplikasi web tersebut. Meski kita nantinya tidak akan menggunakan default setting spring-security ini, tapi ada baiknya kita coba dengan memasukkan “user” pada kolom username tersebut dan password-nya dapat anda temukan pada konsol ketika kita menjalankan aplikasi web ini, kalau di komputer saya tercetak seperti gambar dibawah ini:
Lihatlah ada tulisan default security password dalam konsol tersebut. Silahkan anda cari dalam konsol anda dan masukkan default password tersebut dalam kolom password pada prompt yang keluar tadi.
Dalam spring-security banyak sekali metode autentikasi yang bisa diimplementasikan seperti metode in memory, jdbc, openId, oauth, ldap dan lain-lain. Maksudnya spring-security bisa dikonfigurasi untuk bisa terintegrasi dengan metode-metode diatas, sehingga user-user yang bisa mengakses aplikasi web kita diambil dari sistem-sistem yang kita integrasikan. Lebih lengkapnya silahkan baca dokumentasi tentang spring-security pada link ini. Tapi untuk percobaan kita kali ini kita hanya akan mencoba dua metode saja yaitu “in memory” dan “jdbcbased authentication.
In memory user authentication maksudnya adalah user-user yang bisa mengakses web aplikasi kita di simpan dalam memory ketika aplikasi web kita jalankan, bukan di simpan dalam database, atau sistem lain seperti ldap atau openId atau yang lain. Metode “in memory user authentication” ini banyak digunakan oleh para developer aplikasi web untuk membuat mock-up aplikasi web yang tujuannya untuk presentasi atau dalam ruang lingkup development saja. Keunggulan dari metode ini adalah tidak perlu me-maintain user-user yang bisa mengakses aplikasi web tersebut. Mari kita coba metode ini.

Langkah pertama yang harus kita lakukan supaya spring-security tidak menjalankan fungsi default-nya adalah membuat sebuah class yang berfungsi sebagai class konfigurasi spring-security ini, kita beri nama class ini dengan nama “SecurityConfig” dan kita letakkan dalam packageorg.josescalia.blog.simple.config”. Adapun isi dari class ini adalah sebagai berikut.

package org.josescalia.blog.simple.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
 * Created by josescalia on 26/02/16.
 */

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Override
    protected void configure(HttpSecurity http)throws Exception{
        http.authorizeRequests()
                .antMatchers("/").permitAll() //izinkan semua
                .antMatchers("/css/**").permitAll() //izinkan semua
                .antMatchers("/js/**").permitAll() //izinkan semua
                .antMatchers("/fonts/**").permitAll() //izinkan semua
                .antMatchers("/login").permitAll() //izinkan semua (termasuk default spring-security login page
                .anyRequest().authenticated() //selain yang di atas harus authenticated
                .and()
                .formLogin() //login config
                .and()
                .logout() //logout config
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                ;
    }

    @Autowired
    protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("adi").password("adi123").roles("USER");
        auth.inMemoryAuthentication().withUser("administrator").password("admin123").roles("ADMIN");
    }
}
Ok, perhatikan masing-masing fungsi pada kode-kode diatas, fungsi yang pertama adalah fungsi yang berisi bagaimana kita mengkonfigurasi aplikasi web kita, disana kita lihat url-url seperti “/”, “/css/**”, “/js/**”, “/fonts/**”, “/login”, boleh diakses tanpa harus ada otentikasi. Selain url tersebut, semuanya harus diotentikasi atau memerlukan hak akses untuk bisa masuk ke dalamnya. Kenapa kita set seperti itu untuk url-url-nya, begini penjelasannya, folder css, js, fonts adalah folder yang berisi static-content yang tidak perlu di proteksi, sementara url/login” adalah url-default milik spring-security untuk bisa menampilkan form login. Kalo url-url tersebut kita proteksi tentunya jadi lucu, gimana bisa masuk ke login form, orang login form-nya aja diproteksi, sementara jika folder css, atau js, atau fonts kita proteksi, pastinya tampilan aplikasi web kita akan hancur berantakan tanpa styling dan fungsi-fungsi action javascript yang kita buat sebelumnya tidak akan berfungsi. Kemudian masih pada fungsi yang pertama kita bisa melihat cara kita mengkonfigurasi form login dan url logout disana. Jika user sukses melakukan logout, maka user akan di arahkan ke url/”. 
Kemudian pada fungsi yang kedua kita mendeklarasikan daftar user yang bisa mengakses aplikasi web kita, konfigurasi inilah yang saya maksud dengan “in memory user authentication”. Kita memberikan roleUSER” pada useradi” dan memberikan roleADMIN” pada useradministrator” di fungsi kedua tersebut. Konfigurasi user authentication semacam ini sangatlah  sederhana, kita tidak memerlukan penyimpanan tertentu untuk menampung user-user dengan role-role-nya. Namun kita akan sangat kerepotan jika metode ini kita terapkan dalam aplikasi web yang sebenarnya, sebab biasanya aplikasi web sebenarnya memiliki user management sendiri. Dan kita tidak mungkin meng-coding, meng-compile ulang aplikasi web tersebut setiap kali ada penambahan user atau modikasi role pada masing-masing user.

Langkah selanjutnya silahkan jalankan aplikasi web tersebut, dan jika tidak ada error pada konsol ketika kita menjalankan aplikasi web tersebut, cobalah akses url-url berikut ini, dan kemudian simpulkan sendiri tentang konfigurasi spring-security yang sudah kita buat.

  1. http://localhost:8080/
  2. http://localhost:8080/author/paginated_list
  3. http://localhost:8080/author/edit?id=1
Bagaimana kesimpulan para pembaca semua?, mudah-mudahan ada pengetahuan baru yang didapatkan dari hasil percobaan di atas.


Selanjutnya kita akan mencoba menerapkan metode “Jdbc User Authentication” sebagai implementasi spring-security yang lain dalam aplikasi web kita. "Jdbc User Authentication" adalah suatu metode dalam spring-security dimana user-user yang bisa mengakses suatu aplikasi web disimpan dalam database, dengan cara ini pemeliharaan (maintain) user management akan lebih mudah. “Jdbc User Authentication” memerlukan dua buah tabel dalam database sebagai media penyimpanan data-data user tersebut atau biasa dikenal dengan istilah “user credential”. Oleh karena aplikasi web yang kita buat dari awal sudah mendukung pembuatan tabel secara otomatis berdasarkan object model yang kita definisikan sebagai entity, maka dua tabel yang diperlukan spring-security ini pun akan kita buat menggunakan class-class model.

Class model yang pertama yang akan kita buat dalam rangkan implementasi “Jdbc User Authentication” dalam spring-security ini adalah tabel “users”. Buatlah sebuah class dengan nama “User” dan letakkan class tersebut pada packageorg.josescalia.blog.simple.model”. Adapun isi dari classUser” ini adalah sebagai berikut:

package org.josescalia.blog.simple.model;

import javax.persistence.*;

/**
 * Created by josescalia on 23/03/15.
 */
@Entity
@Table(name = "users")
public class User {

    private String username;
    private String password;
    private Boolean enabled;

    public User() {
    }

    @Id
    @Column(name = "username" , length = 100, unique = true)
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Column(name = "password" , length = 100)
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Column(name = "enabled")
    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", enabled=" + enabled +
                '}';
    }
}
ClassUser” ini memiliki 3 buah properti yaitu username, password, dan enable. Properti-properti ini merupakan properti minimal yang harus dimiliki oleh tabel yang akan dipergunakan dalam implementasi spring-security. Jika dikatakan minimal berarti ada properti-properti yang lain yang bisa digunakan tetapi sifatnya opsional, boleh ditambahkan boleh juga tidak, silahkan cari tahu properti-properti lain yang bisa ditambahkan dalam classUser” tersebut pada situs resmi spring-security. Pada baris kode diatas, kita men-set kolom username pada tabel users nantinya sebagai suatu data yang unique, artinya tidak boleh ada dua user dengan nama yang sama. Dan user ini juga kita set sebagai primary key pada tabel tersebut.
Kemudian class model yang kedua yang harus ada dalam implementasi spring-security adalah “Authorities”. ClassAuthorities” dalam konsep spring-security merupakan object relational yang memiliki foreign key terhadap tabel users namun relasi tabel-nya bersifat many to one atau bisa dipahami dengan skenario “satu data user boleh memiliki banyak data authoritites”. Langsung saja kita buat classAuthorities” ini dan letakkan dalam package yang sama dengan letaknya classUser” sebelumnya. Adapun isi dari classAuthorities” ini adalah sebagai berikut:
package org.josescalia.blog.simple.model;

import javax.persistence.*;
/**
 * Created by josescalia on 23/03/15.
 */
@Entity
@Table(name = "authorities",uniqueConstraints =@UniqueConstraint(columnNames = {"username","authority"}))
public class Authorities {

    private Long id;
    private User user;
    private String authority;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID", length = 30)
    public Long getId() {
        return id;
    }

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

    @ManyToOne
    @JoinColumn(name = "username")
    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    @Column(name = "authority", length = 30, nullable = false)
    public String getAuthority() {
        return authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }

    @Override
    public String toString() {
        return "Authorities{" +
                "user=" + user +
                ", authority='" + authority + '\'' +
                '}';
    }
}
Hal yang mesti kita perhatikan dalam kode-kode di atas adalah relasi antara classAuthoritites” ini dengan classUser”, pada metode getUser kita menggunakan anotasi @ManyToOne untuk menandakan bahwa hubungannya dengan classUser” ini sebagai relasi “many to one”. Kemudian juga perhatikan definisi tabel pada anotasi @Table, disana kita mendefinisikan supaya ada Unique Constraint yang merupakan gabungan kolom “username” dan kolom “authority”, artinya tidak boleh ada data yang sama pada  kolom “username”  dan “authority”, seperti ilustrasi data berikut ini:
Kemudian langkah selanjutnya yang akan kita lakukan adalah mengganti mekanisme ”In memory User Authentication” pada class SecurityConfig menjadi mekanisme “Jdbc User Authentication”, adapun modifikasi yang kita lakukan pada class SecurityConfig adalah seperti berikut ini:
package org.josescalia.blog.simple.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.ArrayList;
import java.util.List;
/**
 * Created by josescalia on 26/02/16.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    DatabaseConfig databaseConfig;

    …

    @Autowired
    protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        /*auth.inMemoryAuthentication().withUser("adi").password("adi123").roles("USER");
        auth.inMemoryAuthentication().withUser("administrator").password("admin123").roles("ADMIN");*/
        JdbcUserDetailsManager userDetailsService = new JdbcUserDetailsManager();
        userDetailsService.setDataSource(databaseConfig.getDataSource());
        org.springframework.security.crypto.password.PasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
        auth.jdbcAuthentication().dataSource(databaseConfig.getDataSource());
    }
}
Sekarang mari kita bahas kode-kode di atas, ada beberapa tambahan pada class SecurityConfig tersebut, yaitu class DatabaseConfig yang di inisialisasi dengan anotasi @Autowired dan import-import package yang diperlukan untuk mengkonfigurasi spring-security dengan metode “Jdbc User Authentication”. Namun coba perhatikan baris-baris di dalam fungsi configureGlobal, kita meng-comment metode “In Memory User Authentication” dan menggantinya dengan “Jdbc User Authentication”. Password untuk user authentication ini kita encode dengan class BcryptPasswordEncoder.
Spring-security menggunakan service JdbcUserDetailsManager dalam menerapkan metode “Jdbc User Authentication” ini. Yang kita perlu lakukan terhadap service tersebut hanya menge-set datasource, dan datasource yang kita set untuk class JdbcUserDetailsManager ini adalah class DatabaseConfig yang sudah kita buat sebelumnya. Dan selanjutnya untuk AuthenticationManager instance dari class JdbcUserDetailsManager ini kita set ke dalam class AuthenticationManagerBuilder bersamaan dengan enkripsi password yang sudah kita definisikan. Dan tentunya kita juga menge-set datasource class AuthenticationManagerBuilder ini ke class DatabaseConfig yang sudah kita buat sebelumnya.
Sekarang langkah selanjutnya adalah jalankan aplikasi web tersebut dan perhatikan log konsol ketika aplikasi web tersebut dijalankan, seharusnya ada log yang menandakan tabel users dan tabel authorities dibuat otomatis oleh aplikasi web kita ke dalam database, seperti gambar dibawah ini:
Dalam log tersebut terlihat aplikasi web kita mendeteksi bahwa tabel users dan tabel authorities tidak ditemukan di dalam database, kemudian aplikasi web kita meng-update database tersebut dengan cara membuatkan tabel users dan table authorities ke dalam database kita. Silahkan pastikan kembali ke dalam database anda, apakah benar tabel users dan table authorities sudah terbuat secara otomatis di dalamnya.
Jika anda mencoba mengakses aplikasi web tersebut dengan kondisi seperti ini, saya jamin anda tidak akan bisa mengakses data-data dalam aplikasi web anda tersebut, kenapa demikian? Sebab beberapa url seperti "author/**" atau "book/**" atau "publisher/**" sudah diproteksi dengan spring-security bukan?, dan anda belum memiliki user dan password untuk bisa mengaksesnya, sebab tabel users dan authorites masih kosong. Bagaimana jika kita mengisi tabel-tabel tersebut secara manual?, tentu anda akan selalu gagal otentikasi ketika mencoba mengakses dengan username dan password yang anda buat secara manual, sebab kita sudah men-set enkripsi password dalam konfigurasi spring-security sebelumnya.
Lalu bagaimana cara kita untuk bisa mengatasi persoalan tersebut? Kita akan membuat sebuah class yang kita beri nama “DataInitializer”. Class ini bertugas memeriksa apakah tabel users dan authorities ada datanya atau tidak, jika tidak ada, kita akan meng-insert data default yang kita tentukan sendiri. Dan class ini harus dijalankan pada saat pertama kali aplikasi web kita boot-up. Trik ini cukup ampuh untuk mengisi aplikasi web kita dengan “pre-defined” data. Kita juga bisa mengisi data-data default seperti data author, data publisher selain data users itu sendiri. Trik ini banyak digunakan para developer aplikasi agar ketika aplikasi web itu pertama kali di-install atau deploy, data di dalamnya tidak kosong sama sekali.
Mari kita buat classDataInitializer” tersebut. Buat classDataInitializer” dan letakkan dalam package yang sama dengan class-class konfigurasi seperti class DatabaseConfig dan class SecurityConfig. Adapun isi dari classDataInitializer” ini adalah sebagai berikut:
package org.josescalia.blog.simple.config;

import org.apache.log4j.Logger;
import org.josescalia.blog.simple.model.Author;
import org.josescalia.blog.simple.model.Book;
import org.josescalia.blog.simple.model.Publisher;
import org.josescalia.blog.simple.repository.AuthorRepository;
import org.josescalia.blog.simple.repository.BookRepository;
import org.josescalia.blog.simple.repository.PublisherRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
 * Created by josescalia on 03/02/16.
 */
@Component
public class DataInitializer {

    static Logger logger = Logger.getLogger(DataInitializer.class.getName());
    
    @Autowired
    DatabaseConfig databaseConfig;

    @PostConstruct  //need to trigger this to fill up first Data
    public void initData(){
        logger.info("Init Data Invoked");
        
        /*harus di taro disini sebab spring-security akan menginisialiasi spring security lebih dahulu, baru kemudian mengeksekusi @PostConstruct untuk pengisian data*/
        JdbcUserDetailsManager userDetailsService = new JdbcUserDetailsManager();
        userDetailsService.setDataSource(databaseConfig.getDataSource());
        org.springframework.security.crypto.password.PasswordEncoder encoder = new BCryptPasswordEncoder();
        if(!userDetailsService.userExists("user")) {
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            UserDetails userDetails = new User("user", encoder.encode("password"), authorities);
            userDetailsService.createUser(userDetails);
        }
        if(!userDetailsService.userExists("administrator")){
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            UserDetails userDetails = new User("administrator",encoder.encode("admin123"),authorities);
            userDetailsService.createUser(userDetails);
        }
     }
}
ClassDataInitializer” ini memiliki satu buah fungsi yang bernama “InitData” dan fungsi ini diberikan anotasi @PostConstruct yang menandakan bahwa class ini akan dijalankan setiap kali aplikasi web kita dijalankan pertama kali. Isi dari fungsi ini kurang lebih sama dengan skenario kita yaitu memeriksa tabel users, jika useradministrator” dan “user” tidak ada dalam database maka buatlah kedua user tersebut dan masukkan ke dalam database. Kita membuat dua buah user default dengan detail user dengan nama “user” memiliki passwordpassword” dan memiliki role sebagai “ROLE_USER”, dan kemudian user dengan nama “administrator” memiliki passwordadmin123” dan memiliki “ROLE_USER” dan “ROLE_ADMIN” sebagai authority-nya. 
Sekarang jalankan kembali aplikasi web kita tersebut, dan cobalah akses url-url yang diproteksi dengan menggunakan username dan password yang sudah kita definisikan sebelumnya pada classDataInitializer”. Sampai disini kita telah selesai mengimplementasi fitur keamanan dalam aplikasi web kita dengan spring-security. Mungkin pada tulisan selanjutnya kita akan mencoba meng-custom tampilan form login spring-security tersebut dan menerapkan level otorisasi hak akses pada url-url aplikasi web kita ini. Untuk source-code latihan pada tulisan ini dapat dilihat dan di akses di sini.

Semoga Bermanfaat

Depok, 28 Februari 2016

Salam
Josescalia 

25 February 2016

Spring-boot, MVC, Freemarker, dan Twitter-Bootstrap (bag 5)

Salam jumpa kembali pembaca, bagaimana pendapat anda tentang beberapa tutorial-tutorial terakhir di blog ini tentang spring-boot, saya berharap ada yang kita dapatkan sebagai pengetahuan dan inspirasi untuk kegiatan komputer kita dari tulisan-tulisan tersebut. Kita sudah mempelajari bagaimana spring-boot menjadi kerangka utama dalam pembuatan aplikasi web, kita juga sudah mempelajari bagaimana integrasi komponen lain seperti freemarker dan bootstrap terhadap sebuah aplikasi web yang dibuat menggunakan spring-boot.
Untuk pembahasan kali ini kita akan sedikit lebih dalam menggali fitur spring-boot dari sisi repository. Seperti yang sudah kita pelajari sebelumnya bahwa dalam bundle spring-boot ada sebuah layer yang bisa kita gunakan sebagai layer dao dengan cara meng-extend class-class spring-data-jpa yang ada pada layer dao kita. Dengan cara ini kita tidak perlu menuliskan fungsi untuk menyimpan (save) , menghapus (delete), membaca (find all) dalam layer dao kita untuk bisa melakukan fungsi-fungsi standar CRUD ke dalam database, cukup meng-extend suatu class milik spring-boot maka kita langsung bisa menggunakan fungsi-fungsi tersebut.
Namun demikian memang tidak semua yang kita inginkan selalu ada, ada beberapa hal yang tidak disediakan oleh framework spring-boot seperti pencarian data dalam database. Beberapa hal yang tidak disediakan dalam spring-boot bisa kita lakukan sendiri dengan membuat fungsi-fungsi yang bisa dikatakan custom. Untuk latihan kita kali ini kita akan mencoba membuat fungsi dao yang kita butuhkan untuk aplikasi web kita. 
Sebagai latihan pertama, yang kita lakukan adalah kita akan menambahkan fitur pencarian pada halaman list Author. Penambahan fitur pencarian ini akan mempengaruhi beberapa class yang kita buat sebelumnya, seperti interface AuthorRepository, AuthorController dan file list author yang terdapat pada foldersrc/main/templates/author”. Kita akan mulai dengan membuat sebuah komponen text box dan tombol pencarian dalam halaman web list data author seperti gambar dibawah ini:
Pada gambar diatas kita melihat ada dua buah komponen baru pada sebelah kanan halaman list data author tersebut, komponen tersebut adalah sebuah text box dan sebuah tombol. Sudah bisa kita duga skenario dari fitur pencarian ini adalah jika user meng-input-kan suatu teks dalam text-box tersebut dan kemudian menekan tombol Search, maka list author pun berubah sesuai dengan kriteria teks yang dimasukkan dalam text-box tersebut, dan pencarian ini akan kita buat dengan menggunakan metode “or”, maksudnya adalah jika user menginputkan kata “Jakarta” maka ketika tombol Search  di-klik, aplikasi web akan mencari kata “Jakarta” tersebut di dalam tabel author, baik itu di kolom authorName ataupun authorAddress. Agar tampilan halaman list data author seperti gambar di atas, maka modifikasi file list.ftl bisa kita lakukan seperti berikut ini:
…
<div class="container">
    <#include "../include/_add_search_component.ftl">
<table class="table table-bordered table-striped table-condensed">
    <thead>
    <tr>
        <th>Action</th>
        <th>Name</th>
        <th>Address</th>
        <th>Created</th>
    </tr>
    </thead>
    <tbody>
    <#list authorList as author>
        <tr>
            <td style="text-align:center;">
                <a href="edit?id=${author.id}" class="btn btn-info btn-sm"><span class="glyphicon glyphicon-edit"></span> </a>
                <a href="#" class="btn btn-danger btn-sm" onclick="deleteData('${author.id}')"><span class="glyphicon glyphicon-trash"></span> </a>
            </td>
            <td>${author.authorName}</td>
            <td>${author.authorAddress}</td>
            <td><#if author.createdDate??>${author.createdDate?string('dd MMM yyyy HH:mm:ss')}</#if></td>
        </tr>
    </#list>
    </tbody>
</table>
</div>
...
Perhatikan baris kedua dalam kode diatas, kita meng-include sebuah komponen yaitu “_add_search_component.ftl” dalam kode di atas, kita sengaja membuatnya menjadi komponen seperti contoh tersebut, sebab sudah pasti nantinya komponen ini akan bisa digunakan di setiap list data, baik itu data Author, data Book, dan data Publisher. Adapun isi dari komponen tersebut adalah sebagai berikut:
<div class="row">
    <div class="col-lg-6 col-md-6 col-sm-6">
        <a href="#modalForm" class="tblAddNew btn btn-info btn-sm" data-toggle="modal" data-placement="top">Add New</a>
    </div>
    <div class="col-lg-6 col-md-6 col-sm-6 form-inline text-right">
        <input type="text" class="form-control" id="searchText" name="searchText" style="width:50%" value="${searchText!""}">
        <button type="button" id="btnFind" class="btn btn-warning btn-sm"><span
                class="glyphicon glyphicon-search"></span> Search
        </button>
    </div>
</div>
<p>&nbsp;</p>
Perhatikan komponen _add_search_component.ftl tersebut, dua komponen tersebut kita namai sebagai searchText dan btnFind, selalu perhatikan penamaan komponen-komponen ini sebab nama-nama ini yang nantinya akan kita libatkan dalam penulisan fungsi-fungsi javascript agar komponen tersebut bisa berfungsi. Kemudian tambahkan kode-kode berikut ini dalam fungsi javascript yang ada pada filelist.ftl”.
$(function(){
    $("#btnSave").click(function(){
        $.post("/service/json/author/save", $("#modalForm").find("form").serialize(), function(jsonString){
            if(jsonString == "Save Succeed"){
                alert(jsonString);
                window.location.reload();
            }else{
                alert(jsonString);
            }
        })
    });
    $("#btnFind").click(function(){
        window.location = "list?" + "searchText="+$("#searchText").val();
    });
});
Penambahan kode pada javascript  tersebut berarti bahwa jika tombol btnFind di-klik oleh user, maka aplikasi web akan mengarahkan ke halaman “http://localhost:8080/author/list?searchText=” atau hanya menambahkan ?searchText= pada url

Perhatikan bahwa url yang di panggil oleh fungsi javascript tersebut sebenarnya tidak berubah melainkan hanya menambahkan sebuah parameter dengan nama searchText, maka pada class AuthorController fungsi getList, harus kita modifikasi menjadi seperti di bawah ini:
@RequestMapping(value = "/author/list", method = RequestMethod.GET)
public String getList(Map<String, Object> objectMap, @RequestParam(required = false) String searchText) {
    if(searchText == null){
        searchText = "";
    }
    objectMap.put("authorList", (List<Author>) authorRepository.findFiltered(searchText));
    objectMap.put("model", new Author());
    objectMap.put("searchText", searchText);
    return "author/list";
}
Hal-hal yang perlu kita perhatikan pada modifikasi tersebut diantaranya adalah penambahan @RequestParam sebagai parameter baru pada fungsi getList tersebut. Penambahan paramater ini bersifat “tidak harus ada” artinya jika url yang dipanggil tidak ada parameter searchText-nya maka fungsi ini akan tetap dijalankan. Kemudian pencabangan jika searchText == null disana di isi oleh nilai string kosong, hal ini dilakukan agar pada layer dao (repository) tidak terjadi error dan tetap bisa diproses di dalam database. Kemudian  juga baris authorRepository.findFiltered(searchText), ini adalah fungsi custom pada layer dao yang akan kita buat nanti di dalam interface AuthorRepository. Dan yang terakhir adalah baris dimana “searchText” tersebut juga tetap dilempar kembali ke layer viewer (freemarker), tujuannya adalah agar text-box searchText tadi tetap ada isinya ketika url berubah. 

Sekarang kita akan memodifikasi interface AuthorRepository dengan cara menambahkan sebuah fungsi dengan nama findFiltered, supaya controller dapat menemukannya. Berikut ini baris modifikasi pada interface AuthorRepository tersebut.
package org.josescalia.blog.simple.repository;
import org.josescalia.blog.simple.model.Author;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
 * Created by josescalia on 25/10/15.
 */
@Repository
public interface AuthorRepository extends CrudRepository<Author,Long> {
    
    @Query(value = "SELECT a from Author a where a.authorName like %:searchText% or a.authorAddress like %:searchText% ")
    List<Author> findFiltered(@Param("searchText") String searchText);
}
Ok, mari kita bahas baris kode tersebut, dalam fungsi findFiltered ini kita melihat ada anotasi @Query, ketika kita membuat fungsi yang sifatnya custom pada layer repository kita di-ijinkan untuk menggunakan Hibernate Query Language (HQL) ataupun Structure Query Language (SQL) di dalamnya, dengan cara seperti ini kustomisasi layer repository bisa dilakukan sesuai dengan kebutuhan aplikasi. Kemudian lihatlah dalam fungsi findFiltered tersebut, fungsi ini memiliki kembalian berupa array list Author dan memiliki paramater searchText yang memiliki tipe data sebagai String. Paramater ini dipakai dalam query yang kita pasangkan di fungsi tersebut, perhatikan baris query-nya, ada kata-kata like %:searchText%, syaratnya adalah paramater dan pemakaiannya pada anotasi query harus benar benar sama (case sensitif).

Sekarang mari kita jalankan aplikasi web kita dan lakukan test terhadap fitur pencarian yang barusan kita buat, bagaimana hasilnya? Pada aplikasi web saya seperti gambar dibawah ini:
Dan akhirnya silahkan anda ambil kesimpulan sendiri dari apa-apa yang sudah kita lakukan tersebut. Saya yakin akan begitu banyak inspirasi yang bisa diwujudkan akan hasil kegiatan kita untuk yang ini. 
Modifikasi yang kedua yang akan kita lakukan adalah kita akan menambah satu fitur lagi pada aplikasi web kita yaitu pagination. Pagination adalah suatu fitur yang bisa membuat sebuah daftar data dibagi menjadi perhalaman, bisa satu halaman 10 data, 20 data dan lain-lain terserah kita. Pada prinsipnya ketika kita menerapkan pagination dalam aplikasi web kita, sebetulnya kita cuma memilih sekian data yang ditampilkan mulai dari baris sekian dalam select query pada database. Kalo di database mysql mungkin anda pernah tahu yang namanya fungsi limit dalam mysql. Namun untuk percobaan ini kita tidak akan menggunakan fitur seperti mysql itu, melainkan kita akan menyerahkannya kepada spring-boot. Bagaimanakah caranya? Silahkan ikuti kembali langkah-langkah dibawah ini.

Interface AuthorRepository sebelumnya meng-extend interface CrudRepository, untuk bisa melakukan pagination dari sisi layer repository, extend ini harus kita rubah menjadi PagingAndSortingRepository. Interface PagingAndSortingRepository merupakan interface perluasan dari CrudRepository sehingga fungsi save, findAll, findOne yang ada dalam interface CrudRepository sudah termasuk di dalam interface PagingAndSortingRepository. Adapun  modifikasi interface AuthorRepository seperti di bawah ini.
package org.josescalia.blog.simple.repository;

import org.josescalia.blog.simple.model.Author;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
 * Created by josescalia on 25/10/15.
 */
@Repository
public interface AuthorRepository extends PagingAndSortingRepository<Author,Long> {

    @Query(value = "SELECT a from Author a where a.authorName like %:searchText% or a.authorAddress like %:searchText% ")
    List<Author> findFiltered(@Param("searchText") String searchText);

    @Query(value = "SELECT a from Author a where a.authorName like %:searchText% or a.authorAddress like %:searchText%")
    Page<Author> getPaginatedList(@Param("searchText") String searchText, Pageable pageable);

}
Kita juga menambahkan suatu fungsi baru dalam interface AuthorRepository yaitu fungsi getPaginatedList, fungsi ini memiliki dua buah paramater yang bernama searchText dan pageable yang memiliki tipe data sebagai Pageable. Dan fungsi ini memiliki kembalian berupa object Page. Pageable dan Page adalah class-class yang digunakan oleh interface PagingAndSortingRepository yang bisa kita gunakan untuk membuat fitur pagination dalam aplikasi web kita. Jika kita bandingkan hibernate query language (HQL) antara fungsi findFiltered dengan fungsi getPaginatedList di atas, tidak ada perbedaan sama sekali diantara keduanya. Penggunaan class Page dan Pageable pada penulisan fungsi saja yang membedakan kedua fungsi tersebut. 

Sebelum kita memodifikasi class AuthorController, kita akan membuat sebuah class baru dulu sebagai representasi object Pagination yang bisa kita pakai dalam layer controller dan viewer nantinya, sebab untuk membuat fitur pagination dalam web aplikasi kita, tentunya ada beberapa info tentang pagination itu sendiri yang harus kita lempar dari layer controller ke layer viewer (freemarker). Info pagination yang dimaksud seperti total page, current page, total seluruh data, dan lain-lain. Jika kita bungkus informasi-informasi tersebut dalam sebuah object tentunya akan sangat efektif pemanfaatan OOP (object oriented programming) kita bukan?. Class baru tersebut kita namakan sebagai Pagination dan kita letakkan dalam package org.josescalia.blog.simple.util seperti gambar dibawah ini.
Adapun isi dari class Pagination tersebut adalah seperti dibawah ini:
package org.josescalia.blog.simple.util;

/**
 * Created by josescalia on 21/02/16.
 */
public class Pagination {
    private Integer page;
    private Integer totalPage;
    private Integer totalRow;
    private Integer totalDisplay;
    public Integer getPage() {
        return page;
    }
    public void setPage(Integer page) {
        this.page = page;
    }
    public Integer getTotalPage() {
        return totalPage;
    }
    public void setTotalPage(Integer totalPage) {
        this.totalPage = totalPage;
    }
    public Integer getTotalRow() {
        return totalRow;
    }
    public void setTotalRow(Integer totalRow) {
        this.totalRow = totalRow;
    }
    public Integer getTotalDisplay() {
        return totalDisplay;
    }
    public void setTotalDisplay(Integer totalDisplay) {
        this.totalDisplay = totalDisplay;
    }
    @Override
    public String toString() {
        return "Pagination{" +
                "page=" + page +
                ", totalPage=" + totalPage +
                ", totalRow=" + totalRow +
                ", totalDisplay=" + totalDisplay +
                '}';
    }

    public static int getStartRowFromStartAndLength(int iDisplayStart, int iDisplayLength) {
        int expectedCalc = 0;
        if (iDisplayStart > 1) {
            expectedCalc = (iDisplayStart / iDisplayLength);
        }
        return expectedCalc;
    }
}
Kita juga menambahkan sebuah fungsi static disana, fungsi ini merupakan kalkulasi untuk menentukan kursor awal query dalam object Pageable nantinya.

Langkah selanjutnya adalah kita akan memodifikasi class AuthorController. Kita putuskan untuk membuat sebuah fungsi baru dengan anotasi @RequestMapping yang juga baru saja kita tentukan dalam fungsi javascript, dan fungsi getList yang menampilkan seluruh data author yang sebelumnya kita tuliskan dalam class AuthorController tidak kita ganggu-ganggu, supaya kita nantinya bisa memahami perbedaan kedua fungsi tersebut. Berikut ini tambahan fungsi baru pada class AuthorController tersebut.
...
@RequestMapping(value = "/author/paginated_list", method = RequestMethod.GET)
public String getPaginatedList(Map<String, Object> objectMap, @RequestParam(required = false) String searchText, @RequestParam(required = false) Integer page) {
    int start = 0;
    int displayLength = 3;
    if(searchText == null){
        searchText = "";
    }
    if(page == null || page == 1){
        start = 0;
        page =1;
    }else if(page > 1){
        start = (page - 1 ) * displayLength;
    }
    int startRow =  getPageFromStartAndLength(start,displayLength);
    
    
    Page<Author> authorPage = authorRepository.getPaginatedList(searchText, new PageRequest(startRow,displayLength));
    Pagination pagination = new Pagination();
    List<Author> authorList = new ArrayList<Author>();
    for (Iterator<Author> iterator = authorPage.iterator(); iterator.hasNext();){
        authorList.add(iterator.next());
    }
    pagination.setPage(page);
    pagination.setTotalPage(authorPage.getTotalPages());
    pagination.setTotalDisplay(displayLength);
    pagination.setTotalRow((int) authorPage.getTotalElements());
    objectMap.put("authorList", authorList);
    objectMap.put("model", new Author());
    objectMap.put("pagination", pagination);
    objectMap.put("searchText", searchText);
    return "author/paginatedList";
}
...
Fungsi yang kita buat diatas hampir sama dengan fungsi getList yang sebelumnya kita buat, hanya saja fungsi ini memiliki satu tambahan parameter lagi yaitu page dengan tipe data Integer dan tidak harus ada ketika url tersebut dipanggil dari dalam browser. Kembalian dari fungsi ini akan memanggil sebuah template freemarker yang bernama paginatedList yang juga terletak dalam folder yang sama dengan fungsi getList, hanya saja nama file template-nya yang berbeda.
Kita deklarasikan beberapa variabel sebagai nilai default seperti start dan displayLength di dalam fungsi tersebut. Dan ada juga beberapa logika sederhana untuk menentukan start dan page jika paramater page tidak ada dalam request http. Perhatikan bagaimana kita membentuk list data author (authorList) dalam fungsi tersebut. Awalnya kita mengambil data dari layer dao dan ditampung dalam variabel authorPage, oleh karena di dalam object authorPage tersebut ada list data author yang diambil dari database, kita ekstrak data tersebut dengan menggunakan metode iterasi dan hasil dari ekstrak data tersebut satu persatu kita masukkan ke dalam variabel authorList yang berbentuk Array List. Kemudian kita juga mendeklarasikan object Pagination di sini dan mengisi nilai-nilainya berdasarkan data-data yang tersedia. Dan yang terakhir kita melempar empat buah variabel dari layer controller ke layer viewer dengan menggunakan object Map, variabel tersebut adalah authorList, model, pagination, dan searchText

Langkah yang terakhir adalah kita membuat file paginatedList.ftl yang merupakan template freemarker dari fungsi yang kita buat ini. Dan tentu saja kita letakkan file paginatedList ini dalam foldersrc/main/resources/templates/author”, adapun isi dari file ini adalah sebagai berikut:
<#import "../layout/main_layout.ftl" as layout>
<@layout.mainLayout>
<div class="container">
    <#include "../include/_add_search_component.ftl">
<table class="table table-bordered table-striped table-condensed ">
    <thead>
    <tr>
        <th>Action</th>
        <th>Name</th>
        <th>Address</th>
        <th>Created</th>
    </tr>
    </thead>
    <tbody>
    <#list authorList as author>
        <tr>
            <td style="text-align:center;">
                <a href="edit?id=${author.id}" class="btn btn-info btn-sm"><span class="glyphicon glyphicon-edit"></span> </a>
                <a href="#" class="btn btn-danger btn-sm" onclick="deleteData('${author.id}')"><span class="glyphicon glyphicon-trash"></span> </a>
            </td>
            <td>${author.authorName}</td>
            <td>${author.authorAddress}</td>
            <td><#if author.createdDate??>${author.createdDate?string('dd MMM yyyy HH:mm:ss')}</#if></td>
        </tr>
    </#list>
    </tbody>
</table>
    <div class="pagination pull-right">
    <#--display next and previous-->
        <a class="btn btn-success btn-xs" <#if pagination.page != 1> href="paginated_list?page=1&searchText=${searchText!""}" </#if>><span class="glyphicon glyphicon-fast-backward"></span> First </a>
        <a class="btn btn-success btn-xs" <#if pagination.page != 1> href="paginated_list?page=${pagination.page - 1}&searchText=${searchText!""}" </#if>><span class="glyphicon glyphicon-chevron-left"></span> Prev </a>
        <a class="btn btn-success btn-xs" <#if pagination.page != pagination.totalPage> href="paginated_list?page=${pagination.page + 1}&searchText=${searchText!""}" </#if>>Next <span class="glyphicon glyphicon-chevron-right"></span>  </a>
        <a class="btn btn-success btn-xs" <#if pagination.page != pagination.totalPage> href="paginated_list?page=${pagination.totalPage}&searchText=${searchText!""}" </#if>><span class="glyphicon glyphicon-fast-forward"></span> Last </a>
    <#--display page info-->
    <p class="text-right text-info">Displaying ${pagination.totalDisplay}  of ${pagination.totalRow} data(s)</p>
    </div>
</div>
<#--author form, hidden by default-->
<div class="modal fade" id="modalForm" tabindex="-1" role="dialog" aria-labelledby="modalLabel" aria-hidden="true"
     style="overflow-y:auto">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header" style="background-color:rgba(173, 216, 230, 0.17)">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true"
                        style="margin:3px">&times;</button>
                <h4 class="text-center">Add New Author</h4>
            </div>
            <div class="modal-body">
                <form class="form-horizontal">
                <#include "_form.ftl">
                </form>
            </div>
            <div class="modal-footer">
                <#include "../include/_add_new_component.ftl">
            </div>
        </div>
    </div>
</div>
</@layout.mainLayout>
<script type="text/javascript">
    $(function(){
        $("#btnSave").click(function(){
            $.post("/service/json/author/save", $("#modalForm").find("form").serialize(), function(jsonString){
                if(jsonString == "Save Succeed"){
                    alert(jsonString);
                    window.location.reload();
                }else{
                    alert(jsonString);
                }
            })
        });
        $("#btnFind").click(function(){
            window.location = "paginated_list?page=${page!1}" + "&searchText="+$("#searchText").val();
        });
    });
    function deleteData(id){
        if(confirm("Apakah anda yakin untuk menghapus data ini ?")){
            $.post("delete", "id=" + id, function(textMessage){
                if(textMessage == "Delete Succeed"){
                    alert(textMessage);
                    window.location.reload(true);
                }
            })
        }
    }
</script>
File template freemarker ini sebenarnya tidak berbeda jauh dengan file template freemarker list.ftl yang kita buat sebelumnya, hanya ada tambahan berupa blok html untuk membuat navigasi paging di sana. Silahkan bandingkan sendiri dengan file list.ftl yang pernah kita buat. Kemudian mari kita test aplikasi web yang sudah kita tambahkan fitur ini dengan menjalankan aplikasi web tersebut dan memanggil urlhttp://localhost:8080/author/paginatedList” pada browser kita. Seharusnya hasilnya seperti gambar dibawah ini:
Pada gambar tersebut kita menampilkan data per-halaman sebanyak 3 buah data, tepat seperti yang kita tentukan di dalam controller, kemudian bentuk navigasi halaman yang sederhana juga tampil disana. Cobalah anda klik salah satu tombol navigasi tersebut dan perhatikan url pada browser, kemudian coba pula cari suatu data dan perhatikan kembali url tersebut, dan ambil kesimpulan yang menurut anda bisa anda pahami dengan baik.

Sampai di sini tutorial kita tentang spring-boot mengenai modifikasi repository telah selesai, tentunya ini masih bisa dikembangkan lebih jauh lagi, dalam percobaan kita memang saya sebagai penulis mencari cara yang paling mudah buat anda pembaca agar lebih mudah memahami tema-tema yang kita pelajari, dan tentunya tetap dalam koridor bisa tetap dikembangkan ke arah yang lebih efektif, efisien, dan modular pastinya. Semoga apa yang kita pelajari dapat kita ambil manfaatnya. Sampai jumpa kembali pada tulisan tutorial spring-boot lainnya.Semoga bermanfaat.

Depok, 25 Februari 2016
Salam,


Josescalia

20 February 2016

Spring-boot, MVC, Freemarker, dan Twitter-Bootstrap (bag 4)

Pada tulisan sebelumnya di serial artikel tutorial ini, kita sudah membahas tentang directives dalam freemarker, dan mudah-mudahan kita semua dapat mengetahui fungsi dan kegunaan directives tersebut serta dapat memanfaatkan pengetahuan tersebut untuk membuat sebuah aplikasi web yang efektif dan efisien. Freemarker sebagai sebuah framework dalam rangkaian desain pattern mvc menempati posisi sebagai viewer atau penampil data dari sebuah aplikasi web. Fitur directives yang sudah kita pelajari sebelumnya menjadi kunci dimana kita bisa membuat komponen-komponen halaman aplikasi web yang reusable.
Dalam dokumentasi freemarker, sebetulnya pemakaian directives pun memiliki referensi bagaimana cara pakai directives tersebut, dan apa yang kita pelajari pada tulisan yang lalu hanya meliputi sedikit saja tentang bagaimana cara memakai directives tersebut. Kita hanya membahas directives yang memang kita perlukan dalam pembuatan sebuah aplikasi web, lebih lanjut jika kita ingin menggali lebih dalam tentang directives, dokumentasi tentang directives tersebut dapat di temukan di sini.
Sekarang kita akan membahas fitur lain dalam freemarker yang tidak kalah penting dengan directives yang pernah kita pelajari sebelumnya. Fitur itu adalah fitur built-in. Fitur ini disediakan oleh freemarker untuk menangani tampilan-tampilan data dalam freemarker template language, fitur  built-in ini dibagi menjadi banyak macamnya dilihat dari sisi pemakaiannya, dokumentasi lengkap tentang fitur built-in ini ada di sini. Dan dalam tulisan ini kita juga hanya membahas beberapa fitur built-in saja, dan tentunya masih dalam koridor pengembangan aplikasi web yang kita sudah kita buat sebelumnya. 
Sekarang kita akan mencoba menggunakan salah satu fitur built-in yaitu datetime. Buatlah sebuah controller baru dengan nama IndexController. Adapun isi dari file controller tersebut adalah seperti dibawah ini:
package org.josescalia.blog.simple.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Date;
import java.util.Map;
/**
 * Created by josescalia on 17/02/16.
 */

@Controller
public class IndexController {

    @RequestMapping("/")
    public String getIndex(Map<String, Object> objectMap){
        objectMap.put("model", new Date());
        return "index";
    }
}
Dalam class controller tersebut kita membuat sebuah fungsi yang akan dipanggil jika ada request pada urlhttp://localhost:8080/”. Bisa dikatakan kita hendak membuat default landing page dalam aplikasi web ini. Kita juga meletakkan object Map dengan variabel objectMap dalam fungsi tersebut yang berfungsi membawa data dari layer controller ke layer viewer (freemarker template). Selanjutnya fungsi ini akan memanggil file index yang terletak di dalam foldersrc/main/resources/templates/”. Dan tentu saja kita juga harus membuat file index.ftl yang kita letakkan dalam foldersrc/main/resources/templates/” tersebut, agar fungsi ini tidak terjadi error ketika kita memanggil url tersebut. Adapun isi dari file index.ftl ini adalah sebagai berikut:
<#import "layout/main_layout.ftl" as layout>
<@layout.mainLayout>
<div class="container">
    <h3>Welcome</h3>
    <p>Date : ${model?string('dd MMM yyyy')}</p>
</div>
</@layout.mainLayout>
Ada yang menarik pada isi file index.ftl tersebut. Yaitu variabel model yang kita bawa dari controller ditambahkan ?string('dd MMM yyyy'), mengapa demikian?. Nilai variabel model yang kita isi pada controller memiliki tipe data java.util.Date. Jika model tersebut dituliskan dalam freemarker seperti apa adanya (${model}) saja, freemarker tidak bisa menerjemahkan langsung object java.util.Date tersebut, sebab object tersebut tidak tergolong object yang displayable pada freemarker template language (ftl), namun freemarker memiliki sebuah fungsi built-in yang bisa membuat object ini menjadi displayable, salah satunya yaitu dengan menambahkan ?string('dd MMM yyyy') dibelakang variabel model. Fungsi built-in ini memerintahkan kepada freemarker untuk menerjemahkan object java.util.Date tersebut sebagai string dan dengan format “dd MMM yyyy”.  Sekarang mari kita jalankan aplikasi web tersebut dan buka urlhttp://localhost:8080/” pada browser. Maka seharusnya tampilan halaman aplikasi web tersebut seperti gambar dibawah ini:

Kita dapat melihat bahwa variabel model dalam halaman aplikasi web tersebut diterjemahkan menjadi tanggal saat ini dengan format 'dd MMM yyyy'. Cobalah anda rubah sendiri format tampilan tanggalnya menjadi format yang lain sesuai dengan referensi Date Format dalam pemrograman java. Selain built-in ?string() diatas, ada juga fitur built-in lain yang bisa menerjemahkan object java.util.Date seperti: ?date, ?datetime, dan lain-lain, silahkan eksplor sendiri mengacu pada dokumentasi referensi freemarker di sini.

Fitur built-in selanjutnya yang akan kita coba adalah built-in for string. Sekarang coba kita modifikasi file IndexController dengan menambahkan baris ini pada fungsi getIndex:
objectMap.put("modelName", "josescalia");
Kemudian file index.ftl kita modifikasi lagi menjadi seperti ini:
<#import "layout/main_layout.ftl" as layout>
<@layout.mainLayout>
<div class="container">
    <h3>Welcome</h3>
    <p>Date : ${model?date}</p>
    <div class="col-lg-6 col-md-6 col-sm-12">
        <table class="table table-bordered table-striped table-condensed">
            <tr>
                <td>Model Normal</td>
                <td class="text-info">${modelName}</td>
            </tr>
            <tr>
                <td>Huruf Depan Kapital</td>
                <td class="text-info">${modelName?capitalize}</td>
            </tr>
            <tr>
                <td>Lower Case</td>
                <td class="text-info">${modelName?lower_case}</td>
            </tr>
            <tr>
                <td>Upper Case</td>
                <td class="text-info">${modelName?upper_case}</td>
            </tr>
            <tr>
                <td>Jumlah Char</td>
                <td class="text-info">${modelName?length}</td>
            </tr>
            <tr>
                <td>3 huruf pertama</td>
                <td class="text-info">${modelName?substring(0,3)}</td>
            </tr>
            <tr>
                <td>3 huruf pertama dengan upper case</td>
                <td class="text-info">${modelName?substring(0,3)?upper_case}</td>
            </tr>
        </table>
    </div>
</div>
</@layout.mainLayout>
Sekarang silahkan jalankan dan lihat hasil modifikasi yang sudah kita lakukan, pada browser saya hasilnya seperti gambar dibawah ini:
Kalo kita perhatikan file index.ftl yang sudah kita modifikasi tadi, sebetulnya built-in string sangat familiar dengan fungsi-fungsi manipulasi string yang biasa kita temui dalam pemrograman java. Jadi seharusnya ini bukan menjadi kendala yang berat buat kita untuk bisa beradaptasi dengan fungsi-fungsi built-in for string pada freemarker template language (ftl) ini.
Pada beberapa tulisan lalu kita sudah berlatih menambahkan fitur tambah data pada aplikasi web kita, tapi kita belum memiliki fitur edit data. Tidak ada salahnya kita membahas penambahan fitur edit data ini pada aplikasi web kita, pengetahuan yang baru kita bahas di atas, bisa kita pakai untuk membuat tampilan data-data pada aplikasi web kita menjadi lebih menarik dan lebih informatif tentunya. Tapi sebelumnya kita akan kembangkan dulu model data Author menjadi seperti dibawah ini:
package org.josescalia.blog.simple.model;

import javax.persistence.*;
import java.util.Date;
/**
 * Created by josescalia on 25/10/15.
 */
@Entity
@Table(name = "author")
public class Author {

    private Long id;
    private String authorName;
    private String authorAddress;
    private Date createdDate;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID", length = 11)
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    @Column(name = "AUTHOR_NAME", length = 100,nullable = false, unique = true)
    public String getAuthorName() {
        return authorName;
    }
    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }
    @Column(name = "AUTHOR_ADDRESS", length = 255,nullable = true)
    public String getAuthorAddress() {
        return authorAddress;
    }
    public void setAuthorAddress(String authorAddress) {
        this.authorAddress = authorAddress;
    }
    @Column(name = "CREATED_DATE", nullable = true)
    public Date getCreatedDate() {
        return createdDate;
    }
    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }
    @Override
    public String toString() {
        return "Author{" +
                "id=" + id +
                ", authorName='" + authorName + '\'' +
                ", authorAddress='" + authorAddress + '\'' +
                ", createdDate=" + createdDate +
                '}';
    }
}
Pada model data Author tersebut kita menambahkan sebuah properti model dengan nama createdDate, dan properti ini memiliki tipe data Date. Perubahan yang kita buat pada class model Author tersebut juga meliputi penambahan getter-setter dan perubahan fungsi toString. Penambahan  baris kode pada fungsi getter dalam class model Author tersebut termasuk mendefinisikan nama field dalam tabel yang nantinya akan terbuat secara otomatis dalam database. Perhatikan kita memberikan attribut boleh null (nullable=true) pada kolom CREATED_DATE tersebut. Ini  sebuah trik yang bisa dilakukan untuk tidak merusak data dalam database jika penambahan fitur tersebut dilakukan ketika data sudah ada sebelumnya dan data tersebut tidak diijinkan untuk dihapus.
Langkah selanjutnya mari kita rubah file list.ftl yang ada pada foldersrc/main/templates/author/” sehingga properti createdDate yang baru kita tambahkan pada model Author dapat tampil pada layer viewer. Adapun perubahan pada file list.ftl ini sebagai berikut:
...
<table class="table table-bordered table-striped table-condensed">
    <thead>
    <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Address</th>
        <th>Created</th>
    </tr>
    </thead>
    <tbody>
    <#list authorList as author>
        <tr>
            <td style="text-align:center;"><a href="edit?id=${author.id}">${author.id}</a></td>
            <td>${author.authorName}</td>
            <td>${author.authorAddress}</td>
            <td>${author.createdDate?date('dd MMM yyyy HH:mm:ss')}</td>
        </tr>
    </#list>
    </tbody>
</table>
</div>
...
Pada kode di atas kita menerapkan pengetahuan yang baru kita pelajari di atas, kita memformat data createdDate sesuai dengan yang kita pelajari untuk bisa tampil dalam halaman aplikasi web kita. Sekarang silahkan jalankan aplikasi web tersebut, dan buka url http://localhost:8080/author/list, bagaimana hasil yang anda dapatkan, seharusnya error seperti gambar saya dibawah ini:
Error di atas terjadi karena memang data createdDate pada tabel author dalam database kita null dan kita sengaja membiarkan null tersebut terjadi dalam database akibat dari penambahan kolom CREATED_DATE yang memperbolehkan null sebelumnya. Dan file list.ftl ini tidak dikondisikan untuk menampilkan data dengan nilai null. Secara logika memang sudah tidak masuk akal, bagaimana mungkin nilai null kita format menjadi suatu tampilan tertentu sesuai dengan yang kita inginkan. Tanpa diformat pun sudah pasti akan error seperti di atas, apalagi diformat.

Lalu bagaimana cara kita menangani permasalahan di atas, tepatnya menangani error akibat data yang null. Freemarker memiliki suatu kemampuan untuk beradaptasi dengan kondisi branching (if else) menggunakan satu baris kode atau biasa kita kenal dengan sebutan one line if. Kita akan menangani permasalahan di atas dengan menerapkan one line if yang pengertiannya kira-kira seperti pseudo code ini “jika data createdDate ada tampilkan dengan format dd MMM yyyy HH:mm:ss dan jika tidak ada kosongkan saja”. Pseudo code tersebut jika kita terjemahkan dalam baris syntax freemarker akan menjadi seperti ini : 
<td><#if author.createdDate??>${author.createdDate?string('dd MMM yyyy HH:mm:ss')}</#if></td>
Dengan kode seperti ini, freemarker akan bisa menerjemahkan dengan baik pseudo code yang kita inginkan. Silahkan buka kembali aplikasi web anda dan lihat hasilnya, pada gambar saya seperti dibawah ini sebab saya memang menambahkan secara manual salah satu data author pada database.

Pada tulisan lalu kita belum membahas bagaimana edit data pada aplikasi web tersebut. Mekanisme edit data memang saya kesampingkan dulu, karena untuk membuat sebuah mekanisme edit data yang efektif dan efisien kita membutuhkan beberapa trik freemarker yang kita pelajari belakangan ini. Jika saya langsung masuk ke dalam mekanisme edit data sebelum kita mempelajari beberapa trik tersebut, pastinya akan banyak pertanyaan. Sekarang saya rasa waktu yang tepat untuk membahas masalah edit data ini.
Kita akan membuat mekanisme edit dan delete data Author sebagai penutup dalam tutorial kali ini. Sebelum kita memulainya, flow berikut ini akan menjadi panduan bagi kita untuk membuat mekanisme edit data tersebut. “Dalam tabel list Author akan ada dua buah tombol action, yg terdiri dari edit dan delete data pada tiap tiap baris data Author. Jika user mengklik tombol delete, maka akan tampil pesan konfirmasi berisi pertanyaan 'Apakah yakin data ini akan di hapus?', jika user mengklik tombol OK pada pesan konfirmasi, maka aplikasi web akan mem-post sebuah http request ke dalam controller yang memanggil fungsi delete, namun jika user mengklik tombol Cancel, maka pesan konfirmasi tersebut akan hilang dan tidak akan melakukan aksi apa-apa. Kemudian jika user mengklik tombol edit pada salah satu baris data Author dalam tabel list Author tadi, maka aplikasi web kita akan mengarahkan kepada halaman edit form Author, kemudian jika user mengklik tombol Save pada form edit data Author tersebut, perubahan data akan disimpan dalam database dan aplikasi akan menampilkan pesan konfirmasi penyimpanan, namun jika user mengklik tombol Cancel pada form tersebut, maka aplikasi web kita akan kembali mengarahkan user ke halaman yang berisi tabel list Author”.
Untuk mendukung flow ini,  langkah pertama yang harus kita lakukan adalah memodifikasi tabel list Author sehingga bisa menjadi seperti gambar dibawah ini:
Dalam tampilan tabel tersebut kita merubah header tabel paling kiri menjadi “Action” dan setiap baris pada data Author memiliki dua tombol yaitu edit dan delete dengan icon-icon bawaan bootstrap. Untuk merubah tampilan seperti ini yang kita lakukan adalah meng-edit filelist.ftl” yang ada pada foldersrc/main/resources/template/author/” menjadi seperti dibawah ini:
...
<table class="table table-bordered table-striped table-condensed">
    <thead>
    <tr>
        <th>Action</th>
        <th>Name</th>
        <th>Address</th>
        <th>Created</th>
    </tr>
    </thead>
    <tbody>
    <#list authorList as author>
        <tr>
            <td style="text-align:center;">
                <a href="edit?id=${author.id}" class="btn btn-info btn-sm"><span class="glyphicon glyphicon-edit"></span> </a>
                <a href="#" class="btn btn-danger btn-sm" onclick="deleteData('${author.id}')"><span class="glyphicon glyphicon-trash"></span> </a>
            </td>
            <td>${author.authorName}</td>
            <td>${author.authorAddress}</td>
            <td><#if author.createdDate??>${author.createdDate?string('dd MMM yyyy HH:mm:ss')}</#if></td>
        </tr>
    </#list>
    </tbody>
</table>
...
Perhatikan perubahan yang kita lakukan pada file tersebut, kita hanya menggunakan tag a dan menghiasnya menjadi tombol dengan class-class yang dimiliki bootstrap. Namun pada tombol edit kita menambahkan sebuah attribut onclick, dimana attribut itu berfungsi jika tombol delete itu diklik maka akan memanggil sebuah fungsi javascript yaitu deleteData dan fungsi ini memiliki sebuah parameter. Kemudian kita teruskan memodifikasi file list tersebut, yaitu menambahkan sebuah fungsi javascript dengan nama deleteData seperti contoh berikut ini:
<script type="text/javascript">
    ...
    function deleteData(id){
        if(confirm("Apakah anda yakin untuk menghapus data ini ?")){
            $.post("delete", "id=" + id, function(textMessage){
                if(textMessage == "Delete Succeed"){
                    alert(textMessage);
                    window.location.reload(true);
                }
            })
        }
    }
</script>
Yap, kita sudah membuat fungsi javascript deleteData tersebut. Kemudian perhatikan kembali pada file list tentang penambahan tombol-tombol pada tabel list Author lagi, ada tombol edit disana yang jika user mengklik tersebut, maka aplikasi web akan mengarahkan ke halaman dengan urlhttp://localhost:8080/author/edit?id=”. Dua perubahan ini harus kita akomodir dengan menambahkan dua fungsi dalam controller yaitu fungsi edit dan fungsi delete dengan request mapping berbeda satu sama lain. Tambahkan dua fungsi dibawah ini pada class AuthorController.
@RequestMapping(value = "/author/edit", method = RequestMethod.GET)
public String edit(@RequestParam Long id, Map<String, Object> objectMap) {
    Author author = authorRepository.findOne(id);
    objectMap.put("model", author);
    return "/author/edit";
}

@RequestMapping(value = "/author/delete", method = RequestMethod.POST)
public @ResponseBody String delete(@RequestParam Long id) {
    try{
        authorRepository.delete(id);
        return "Delete Succeed";
    }catch (Exception e){
        e.printStackTrace();
        return "Delete Failed :" + e.getMessage();
    }
}
Fungsi edit yang kita buat pada class AuthorController di atas, memiliki dua buah paramater. Paramater yang pertama adalah paramater yang merupakan url paramater (edit?id=) dan parameter kedua merupakan paramater dengan tipe data Map yang menjadi penghubung antara controller dengan freemarker seperti yang pernah kita pelajari sebelumnya, dan fungsi ini akan dipanggil jika http get request mengarah pada urlhttp://localhost:8080/author/edit”. Fungsi ini berisi bagaimana cara aplikasi web mengambil data dari dalam database dengan parameter id melalui object authorRepository, kemudian data yang diambil dari database tersebut dimasukkan ke dalam variabel model dan diletakkan pada object Map untuk di bawa ke layer viewer (freemarker). Kemudian kembalian dari fungsi edit ini akan meminta kepada aplikasi web untuk menggunakan sebuah file freemarker template bernama “edit” dalam folder/src/main/templates/author/” sebagai layer viewer-nya.
Sementara fungsi delete dalam class AuthorController diatas memiliki sebuah paramater dengan tipe data Long dan akan dipanggil jika ada sebuah http post request dengan alamat “http://localhost:8080/author/delete”, fungsi ini akan mengembalikan sebuah teks yang akan dicetak dalam halaman aplikasi web dan tidak memerintahkan aplikasi web untuk menampilkan file freemarker sebagai viewer-nya, melainkan hanya mencetak langsung string tersebut. Dalam fungsi delete ini terlihat bahwa proses delete data dalam database akan dilakukan melalui object authorRepository dengan paramater id, dan proses ini ada dalam blok try-catch untuk mencegah kemungkinan error yang terjadi. Jika proses penghapusan data berhasil maka fungsi ini akan mengembalikan sebuah teks “Delete Succeed” dan jika gagal (terjadi error) akan mengembalikan teks “Delete Failed” beserta dengan alasan kenapa tidak bisa menghapus data tersebut.
Perhatikan perbedaan metode HTTP di dalam dua fungsi tersebut, fungsi edit menggunakan HTTP GET, dan fungsi delete menggunakan metode HTTP POST, pertimbangan menggunakan dua metode HTTP yang berbeda lebih kepada konsep sedikit mengamankan data ketika terjadi proses penghapusan data. Seperti yang mungkin sama-sama kita ketahui bahwa paramater yang di-supply ketika menggunakan HTTP POST tidak bisa dituliskan langsung dalam URL browser, sebab jika dituliskan aplikasi web akan mengenali sebagai metode HTTP GET, dan proses penghapusan tidak akan pernah terjadi. Jadi proses penghapusan data ini untuk sebagian user awam dengan aksi hapus data yang hanya bisa dilakukan lewat aplikasi web. Kedua perbedaan metode ini juga mengakibatkan pemakaian fungsi yang berbeda pada fungsi deleteData dalam javascript yang kita tuliskan sebelumnya.
Kemudian yang kita lakukan selanjutnya adalah membuat fileedit.ftl” dan kita letakkan di dalam folder/src/main/templates/author/”, file ini merupakan file tujuan dari metode edit pada controller yang kita buat tadi. Adapun isi dari file edit.ftl ini adalah sebagai berikut:
<#import "../layout/main_layout.ftl" as layout>
<@layout.mainLayout>
<div class="col-lg-6 col-md-6 col-sm-12">
    <form method="post" id="editForm" class="form-horizontal">
        <div class="panel panel-info">
            <div class="panel-heading">
                <h4>Edit Author</h4>
            </div>
            <div class="panel-body">
                <#include "_form.ftl">
                <input type="hidden" id="id" name="id" value="${model.id}">
            </div>
            <div class="panel-footer text-right">
                <#include "../include/_edit_component.ftl">
            </div>
        </div>
    </form>
</div>
</@layout.mainLayout>
<script type="text/javascript">
    $(function(){
        $("#btnUpdate").click(function(){
            $.post("/service/json/author/save", $("#editForm").serialize(), function(responseText){
                alert(responseText);
                if(responseText == "Save Succeed"){
                    window.location = "list"
                }
            });
        });
    })
</script>
Pada file tersebut, kita banyak me-reuse komponen yang kita buat. Seperti komponen form dan komponen _edit_component.ftl. Kita memang belum membuat komponen  _edit_component.ftl, namun sebetulnya komponen tersebut tidak berbeda jauh dengan komponen _add_new_component.ftl yang kita buat sebelumnya, hanya saja id dari tombol Save pada komponen tersebut kita ganti menjadi btnUpdate, adapun isi lengkap dari file _edit_component tersebut seperti dibawah ini:
<a href="#" id="btnUpdate" class="btn btn-info btn-sm"><span class="glyphicon glyphicon-floppy-disk"></span> Update</a>
<a href="#" id="btnCancel" class="btn btn-danger btn-sm" onclick="history.back()"><span class="glyphicon glyphicon-step-backward"></span> Back</a>
Relatif sama bukan?, untuk btnCancel kita kasih sentuhan javascripthistory.back()”, sentuhan ini akan membuat aplikasi web akan kembali ke halaman sebelumnya jika tombol Cancel di klik oleh user. Kita kembali ke file edit.ftl tadi, perhatikan bahwa ada setelah baris include _form.ftl, kita menyelipkan sebuah komponen hidden dengan nama “id”. Komponen ini bertugas menampung “id” dari data Author. Dan oleh karena id author merupakan primary key dalam model data Author, maka sudah seharusnya nilai dari id ini tidak boleh berubah, dan untuk mengakomodir tidak dibolehkannya id tersebut berubah itulah kita menggunakan komponen html hidden, ada komponennya namun tidak terlihat dari sisi user.
Kemudian pada bagian bawah file edit.ftl tersebut kita tuliskan sebuah fungsi javascript. Fungsi ini memerintahkan aplikasi web melakukan HTTP POST Request ke URL/service/json/author/save” dan kemudian kita tangkap kembalian dari metode HTTP POST tersebut ke dalam sebuah alert dan kondisi jika kembalian teks berisi “Save Suceed” maka kita arahkan kembali ke halaman list.
Sebelum kita menjalankan aplikasi web tersebut, ada hal lagi yang mesti kita modifikasi, yaitu file _form.ftl. Perhatikan isi file _form.ftl, semua komponen html yang berbentuk input text box belum kita isi nilainya, artinya kalo kita langsung jalankan aplikasi web kita tanpa merubah file _form.ftl tersebut, maka niscaya hasilnya akan seperti gambar di bawah ini:
Kenapa kok kosong, tidak ada data, padahal dari controller sudah di lemparkan variabel dengan nama “model” lewat object Map?. Ya karena variabel “model” tersebut belum di-apply ke masing-masing komponen. Nah sekarang tinggal kita apply saja variabel model tersebut ke dalam masing-masing komponen pada file _form.ftl ini, menjadi seperti berikut ini:
<div class="form-group">
    <label for="authorName" class="col-lg-4 col-md-6 col-sm-12">Author Name</label>
    <div class="col-lg-8 col-md-6 col-sm-12">
        <input type="text" class="form-control" id="authorName" name="authorName" 
           value="${model.authorName}">
    </div>
</div>
<div class="form-group">
    <label for="authorAddress" class="col-lg-4 col-md-6 col-sm-12">Address</label>
    <div class="col-lg-8 col-md-6 col-sm-12">
       <textarea id="authorAddress" name="authorAddress" class="form-control">
          ${model.authorAddress}</textarea>
    </div>
</div>
Lihatlah pada kode di atas bagaimana cara kita apply model terhadap komponen-komponen tersebut. Sama seperti kita meng-apply komponen hidden pada file _edit.ftl. Sekarang silahkan jalankan aplikasi web tersebut dan cobalah pilih salah satu data pada tabel Author untuk di edit, jalankan fungsinya secara normal, apakah semuanya sudah sesuai dengan skenario kita?

Tapi tunggu dulu, apakah kita telah selesai? saya katakan meski sudah berhasil melakukan edit data dan fungsinya sudah berjalan normal sesuai skenario yang kita buat, tapi kita belum selesai, cobalah klik tombol Add New pada halaman list, apakah form tambah data akan keluar?, jika tidak keluar coba lihat log-nya, seharusnya ada log seperti dibawah ini kira-kira :
FTL stack trace ("~" means nesting-related):
- Failed at: ${model.authorName}  [in template "author/_form.ftl" at line 4, column 90]
- Reached through: #include "_form.ftl"  [in template "author/list.ftl" at line 41, column 17]
~ Reached through: #nested  [in template "layout/main_layout.ftl" in macro "mainLayout" at line 17, column 9]
~ Reached through: @layout.mainLayout  [in template "author/list.ftl" at line 2, column 1]
----] with root cause

freemarker.core.InvalidReferenceException: The following has evaluated to null or missing:

==> model  [in template "author/_form.ftl" at line 4, column 92]

Wah, ternyata ada error dibelakang aplikasi web kita, yup error itu terjadi karena freemarker tidak sanggup mencetak nilai model.authorName dan model.authorAddress yang null, sebab memang pas pada saat kita ingin buat sebuah data Author yang baru, object model sudah didefinisikan di form-nya, padahal di controller-nya tidak ada variabel model yang dilempar ke layer viewer. Lalu bagaimana cara kita menangani hal ini?, Ok langsung saja kita lakukan. Yang pertama kita modifikasi file AuthorController, pada fungsi getList tambahkan baris kode menjadi seperti dibawah ini:
…
@RequestMapping(value = "/author/list", method = RequestMethod.GET)
public String getList(Map<String, Object> objectMap) {
    objectMap.put("authorList", (List<Author>) authorRepository.findAll());
    objectMap.put("model", new Author());
    return "author/list";
}
…
Yang kedua modifikasi kembali file _form.ftl menjadi seperti dibawah ini:
<div class="form-group">
    <label for="authorName" class="col-lg-4 col-md-6 col-sm-12">Author Name</label>
    <div class="col-lg-8 col-md-6 col-sm-12">
        <input type="text" class="form-control" id="authorName" name="authorName" 
         value="${model.authorName!""}">
    </div>
</div>
<div class="form-group">
    <label for="authorAddress" class="col-lg-4 col-md-6 col-sm-12">Address</label>
    <div class="col-lg-8 col-md-6 col-sm-12">
       <textarea id="authorAddress" name="authorAddress" class="form-control">
           ${model.authorAddress!""}</textarea>
    </div>
</div>
Perhatikan file _form.ftl tersebut, kita menambahkan !”” pada masing-masing pemakaian attribut model dalam setiap komponen. Tambahan ini memungkinkan freemarker akan mencetak character kosong jika properti object Author memiliki nilai null, pseudo code-nya begini kira kira “jika properti model Author tidak sama dengan null, maka cetaklah ke dalam komponen tersebut, namun jika null, cetak saja karakter kosong pada halaman aplikasi web tersebut".

Silahkan dicoba sendiri hasilnya. Seharusnya tidak ada lagi masalah, pada saat tambah atau edit data Author.  Demikianlah pembahasan tentang penggunaan fungsi built-in pada freemarker ditambah dengan pelengkapan mekanisme “edit” dan “delete” pada aplikasi web kita tersebut. Mudah-mudahan tutorial ini dapat memberikan pengertian dan pengetahuan lebih mendalam tentang spring-boot pada khususnya. Untuk tulisan tentang spring-boot selanjutnya, kita akan membahas tentang memodifikasi spring repository, modifikasi ini sangat berguna jika kita hendak melakukan operasi data yang tidak sekedar CRUD (Create, Read, Update, Delete) saja. 

Semoga bermanfaat.
Depok, 21 Februari 2016


Josescalia