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

No comments: