Belajar Membuat Aplikasi Web Dengan Spring Framework

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

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

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

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

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

Struktur package di aplikasi web

Struktur package di aplikasi web

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

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

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

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

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

Validasi secara otomatis

Validasi secara otomatis

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

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

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

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

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

}

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

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

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

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

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

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

  @Inject
  private WebFlowConfig webFlowConfig;

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

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

  ...
}

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

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

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

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

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

    return emf;
  }

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

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

}

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

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

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

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

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

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

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

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

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

Tampilan pesan kesalahan pada Ajax

Tampilan pesan kesalahan pada Ajax

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

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

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

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

Contoh flow di SWF

Contoh flow di SWF

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

<flow ... abstract="true">

    <input name="selectedEntity" />    

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

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

    <end-state id="kembali" />

</flow>

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

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

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

  <view-state id="entry">

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

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

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

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

  </view-state>       

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

</flow>

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

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

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

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

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

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

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

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

Contoh tampilan data entry

Contoh tampilan data entry

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

Contoh tampilan edit

Contoh tampilan edit

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

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

Contoh tampilan tampilan tabel

Contoh tampilan tampilan tabel

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

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

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

Untuk membuat persistence layer, saya cukup membuat interface seperti:

public interface KonsumenRepository extends JpaRepository<Konsumen, Long> {

  Page findByNamaLike(String nama, Pageable pageable);

  Page findByAlamatLike(String alamat, Pageable pageable);

  Page findByKota_NamaLike(String namaKota, Pageable pageable);

}

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

Pada service layer, saya membuat class seperti:

@Service @Transactional
public class KonsumenService {

  @Inject
  private transient KonsumenRepository konsumenRepository;

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

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

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

}

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

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

@Component
public class KonsumenAction {

  @Inject
  private KonsumenService konsumenService;

  ...

}

Saya kemudian bisa memakainya seperti pada:

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

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

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

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

Tampilan pesan informasi melalui growl

Tampilan pesan informasi melalui growl

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

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

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

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

Spring Profile Dan Auto Reconfiguration Dari Cloud Foundry

Saya mengalami sebuah dilema karena fitur auto reconfiguration Cloud Foundry tidak bekerja di aplikasi Spring saya.  Pada saat saya men-deploy aplikasi ke server Cloud Foundry, harusnya nilai seperti username, password, dan nama database pada bean data source akan secara otomatis berubah sesuai dengan yang ada di server Cloud Foundry (yup! saya tidak ingin tahu user atau password di server yang merupakan kombinasi angka & huruf!).  Tapi yang terjadi masalah kesalahan NullPointerException.

Sementara, bila saya mengubah nilai username, password, nama database, dan URL di bean data source agar sesuai dengan yang ada di server Cloud Foundry, maka saya tidak bisa lagi menguji aplikasi di komputer lokal karena database di komputer lokal memiliki nama user, password, nama database, dan URL yang berbeda!!

Okay, pilihan terakhirnya adalah setiap kali saya men-deploy, saya harus mengedit bean data source agar nilainya sesuai dengan database MySQL yang ada di server Cloud Foundry.  Begitu selesai men-deploy, saya harus mengembalikan nilai agar sesuai dengan database MySQL yang ada di komputer lokal!  Wew, ini akan sangat merepotkan, serasa hidup di dunia primitif.  Pada dasarnya, saya adalah seorang yang malas dan ingin cepat, jadi apakah tidak ada pilihan lain yang bisa mempermudah hidup saya?

Berikut ini adalah garis besar file konfigurasi Spring saya:

<beans ...>
  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="emf" />
  </bean>

  <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" depends-on="jpaProperties">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
       <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    </property>
    <property packagesToScan="com.snake.aplikasi.domain" />
    <property name="jpaProperties" ref="jpaProperties" />
  </bean>

  <bean id="parentJpaProperties" abstract="true" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
     <property name="properties">
          <!-- 
           ...
           Berisi JPA properties yang berlaku untuk semua jenis deployment.
           ...
          -->
     </property>
  </bean>

  <bean profile="dev,production">
     <!-- Memakai Flyway untuk migrasi perubahan database secara otomatis -->
     <bean id="flyway" class="com.googlecode.flyway.core.Flyway" init-method="migrate">
         <property name="dataSource" ref="dataSource" />
     </bean>

     <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
         <!--
           ...
           Berisi URL, username, dan password untuk database di komputer lokal (development) dan production
           ...
         -->
     </bean>

     <!-- Berisi konfigurasi JPA untuk database di development maupun production -->
     <bean id="jpaProperties" parent="parentJpaProperties" depends-on="flyway">
        <property name="properties">
           <props merge="true">
               <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
           </props>
        </property>
     </bean>
  </bean>

  <bean profile="test">
      <!-- Selama pengujian, tidak memakai database MySQL tetapi memakai database embedded H2 -->
      <jdbc:embedded-database id="dataSource" type="H2" />

      <bean id="jpaProperties" parent="parentJpaProperties">
          <!-- 
             ...
             Berisi konfigurasi JPA khusus untuk database pengujian (database embedded H2)
             ...
           -->    
      </bean>
  </bean>
</beans>

Saya memakai profiles untuk membagi konfigurasi ke dalam 3 profile, yaitu dev, production, dan test.  Khusus untuk bagian database ini, saya menyamakan antara dev dan production (sama-sama memakai database MySQL server!).  Sementara bila JUnit dijalankan untuk pengujian, maka yang aktif adalah profil test sehingga database yang dipakai bukan MySQL melainkan sebuah database embedded H2.

Saya berharap bahwa profil yang aktif pada saat aplikasi telah di-deploy di Cloud Foundry adalah profil production, lalu fitur auto reconfiguration akan secara otomatis akan merubah bean data source saya sesuai dengan user, password dan URL database MySQL di server.   Tapi hal ini tidak terjadi!

Tunggu dulu… Bagaimana saya bisa yakin bahwa profil production akan aktif  di Cloud Foundry nanti?  Atau dengan kata lain, apa profil yang akan aktif bila saya tidak menentukannya?

Setelah melakukan penelusuran dokumentasi Cloud Foundry, akhirnya saya menemukan bahwa profil yang akan aktif secara otomatis pada saat aplikasi di-deploy di Cloud Foundry adalah profil cloud.   Pantas saja!  Sekarang, saya menambahkan pilihan profil baru seperti berikut ini:

<beans profile="dev,production,cloud">
   ...
</beans>

Tapi ini belum menyelesaikan masalah.  Kesalahan tetap muncul!

Akhirnya setelah melakukan penelusuran lebih lanjut, saya menemukan bahwa masalahnya terletak di hierarki bean jpaProperties dan bean parentJpaProperties yang saya pisah (dimana parentJpaProperties berlaku untuk semua profil).  Sepertinya, fitur auto reconfiguration dari Cloud Foundry tidak dapat membaca hierarki seperti ini dengan baik.

Solusi yang saya tempuh adalah dengan menyatukan kedua bean tersebut, sehingga definisi bean saya akan terlihat seperti:

<beans ...>
  <bean id="transactionManager">
    <property name="entityManagerFactory" ref="emf" />
  </bean>

  <bean id="emf" depends-on="jpaProperties">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
       <bean />
    </property>
    <property packagesToScan="com.snake.aplikasi.domain" />
    <property name="jpaProperties" ref="jpaProperties" />
  </bean>

  <bean profile="dev,production,cloud">
     <!-- Memakai Flyway untuk migrasi perubahan database secara otomatis -->
     <bean id="flyway" init-method="migrate">
         <property name="dataSource" ref="dataSource" />
     </bean>

     <bean id="dataSource">
         <!--
           ...
           Berisi URL, username, dan password untuk database di komputer lokal (development) dan production
           ...
         -->
     </bean>

     <!-- Berisi konfigurasi JPA untuk database di development maupun production -->
     <bean id="jpaProperties" depends-on="flyway">
        <property name="properties">
           <props>
               <!-- Semua prop yang sebelumnya ada di parentJpaProperties diletakkan di sini -->
               <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
           </props>
        </property>
     </bean>
  </bean>

  <bean profile="test">
      <!-- Selama pengujian, tidak memakai database MySQL tetapi memakai database embedded H2 -->
      <jdbc:embedded-database id="dataSource" type="H2" />

      <bean id="jpaProperties">
          <!-- 
             ...
             Berisi konfigurasi JPA khusus untuk database pengujian (database embedded H2),
             termasuk semua prop yang sebelumnya ada di parentJpaProperties.
             ...
           -->    
      </bean>
  </bean>
</beans>

Akhirnya fitur auto-reconfiguration dari Cloud Foundry bisa bekerja seperti seharusnya, dan keinginan saya untuk melakukan deploy ke Cloud Foundry dengan hanya menekan satu tombol di Spring Tool Suite bisa terwujud.

Melakukan Front-End Testing Dengan Selenium Tanpa “Mengotori” Database

Pada tulisan Menguji Halaman Web Secara Otomatis, saya menuliskan bagaimana cara memakai Selenium untuk menguji web front end secara otomatis.   Permasalahan yang timbul dengan memakai cara tersebut adalah selama pengujian, saya harus menjalankan aplikasi di server development secara manual dan memakai database yang sama dengan yang dipakai selama development.  Hal ini akan menyebabkan masalah konsistensi data jika seandainya pengujian gagal di tahap tertentu.  Selain itu, bila saya menguji halaman seperti Pemesanan, saya harus menambah Stok baru, menambah User baru, kemudian membuka halaman Pemesanan, lalu menghapus Stok, dan menghapus User.

Langkah yang ditempuh sangat melelahkan sekali, membuat saya menetaskan keringat dingin saat menulis kode program untuk pengujian web front end.  Entah bagaimana para extreme programmer dengan Test Driven Development (TDD)-nya bisa bertahan hidup.  Akhirnya muncul sebuah ide untuk memakai database in-memory yang terpisah dari database development.   Selain itu, saya ingin mengisi database tersebut dengan data secara otomatis dan konsisten setiap kali sebuah method di test class dikerjakan.  Dengan demikian, saya bisa langsung berkonsentrasi pada halaman yang akan diuji, tanpa perlu menambah item-item yang diperlukan di halaman lain.   Saya tidak tahu apakah ini masih masuk dalam kategori functional testing atau unit testing, tapi cara ini akan mempermudah hidup saya (lagipula saya bukan penganut extreme programming!).

Ok, teknologi apa yang saya butuhkan?

  1. Jetty untuk menjalankan aplikasi web secara otomatis selama pengujian front-end.
  2. H2 Database sebagai database in-memory embedded untuk pengujian.
  3. DbUnit untuk mengisi database H2 berdasarkan data dari file Microsoft Excel yang disediakan.  Data  di-isi secara otomatis  setiap kali method yang dikerjakan (dengan kata lain, isi database akan kembali sama seperti isi file Microsoft Excel setiap kali sebuah method dikerjakan).
  4. Selenium, tentunya, untuk melakukan pengujian di browser.

Untuk men-download semua artifact JAR yang dibutuhkan secara otomatis, saya akan menambahkan dependency berikut ini pada Maven:

  1. org.eclipse.jetty.aggregate : jetty-all : 7.6.7.v20120910 [test]
  2. org.apache.tomcat : juli : 6.0. 36 [test] (karena saya memakai implementasi JSP dari Tomcat)
  3. com.h2database : h2 : 1.3.167
  4. org.dbunit : dbunit : 2.4.8 [test]
  5. org.apache.poi : poi : 3-2-FINAL [test]
  6. org.seleniumhq.selenium : selenium-java : 2.25.0 [test]
  7. org.seleniumhq.selenium : selenium-server : 2.25.0 [test]

Struktur proyek yang saya pakai adalah struktur proyek Maven yang terlihat seperti pada gambar berikut ini:

Struktur Proyek

Struktur Proyek

Saya akan meletakkan kode program proyek di src/main/java dan file konfigurasi yang dibutuhkan di src/main/resources.  Khusus untuk kode program yang mewakili test case pengujian, saya meletakkannya di src/test/java.  File-file yang dibutuhkan untuk pengujian akan saya letakkan di src/test/resources.   Dengan struktur proyek seperti ini, pada saat aplikasi akan di-deploy atau di-built, hanya folder src/main saja yang akan disertakan (karena src/test untuk pengujian bukan untuk dipakai user).

Berikutnya saya akan membuat sebuah class bernama AbstractSeleniumTest.  Class ini nantinya wajib di-extends oleh semua class yang akan melakukan pengujian front end.  Isi dari class AbstractSeleniumTest adalah:

import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.List;

import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.util.fileloader.XlsDataFileLoader;
import org.eclipse.jetty.plus.jndi.EnvEntry;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverBackedSelenium;
import org.openqa.selenium.firefox.FirefoxDriver;

import com.thoughtworks.selenium.Selenium;

public class AbstractSeleniumTest {

	protected static Server server;
	protected static DataSourceDatabaseTester dataTester;
	protected static JdbcDataSource dataSource;

	protected Selenium selenium;	

	@BeforeClass
	public static void beforeTestClass() throws Exception {

		// Setup Database H2 In-Memory
		if (dataSource==null) {
			dataSource = new JdbcDataSource();
			dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
			dataTester = new DataSourceDatabaseTester(dataSource);
		}

		// Setup Server Jetty
		if (server==null || !server.isRunning()) {
			Server server = new Server(8181);
			server.setAttribute("org.eclipse.jetty.webapp.Configuration",
				new String[]{"org.eclipse.jetty.plus.webapp.EnvConfiguration"});
			EnvEntry envEntry = new EnvEntry("java:comp/env/jdbc/testdatabase", dataSource);
			server.addBean(envEntry);

			WebAppContext context = new WebAppContext();
			context.setDescriptor("../FolderProyek/src/main/webapp/WEB-INF/web.xml");
			context.setResourceBase("../FolderProyek/src/main/webapp");
			context.setContextPath("/proyek");
			context.setParentLoaderPriority(true);

			List listOverrideDescriptors = new ArrayList();
			listOverrideDescriptors.add("override-web.xml");
			context.setOverrideDescriptors(listOverrideDescriptors);

			server.setHandler(context);			
			server.start();
		}
	}

	@AfterClass
	public static void afterTestClass() throws Exception {
	}

	@Before
	public void setUp() {

		// Database Setup
		XlsDataFileLoader xlsDataFileLoader = new XlsDataFileLoader();
		DataSets dataSetsAnnotation = this.getClass().getAnnotation(DataSets.class);
		if (dataSetsAnnotation!=null) {
			IDataSet dataSet = xlsDataFileLoader.load(dataSetsAnnotation.setUpDataSet());
			dataTester.setDataSet(dataSet);
			try {			
				dataTester.onSetup();
			} catch (Exception ex) {
				fail("Terjadi kesalahan [" + ex.getMessage() + "]");
			}
		}

		// Selenium Setup
		WebDriver driver = new FirefoxDriver();
		String baseUrl = "http://localhost:8181/";
		selenium = new WebDriverBackedSelenium(driver, baseUrl);
	}

	@After
	public void tearDown() {
		if (dataTester!=null) {
			try {
				dataTester.onTearDown();
			} catch (Exception ex) {
				fail("Terjadi kesalahan [" + ex.getMessage() + "]");
			}
		}
		if (selenium!=null) {
			selenium.stop();
		}
	}
}

Kode program pada method beforeTestClass() yang diberi annotation @BeforeClass akan dikerjakan oleh JUnit setelah instance class untuk pengujian dibuat.  Di method ini, saya membuat sebuah database in-memory H2, lalu membuat embedded  Jetty dan melewatkan data source H2 melalui JNDI sehingga bisa dipergunakan oleh aplikasi yang diuji yang berjalan di dalam Jetty.   Pada method ini saya melakukan penjagaan null untuk memastikan bahwa database dan server Jetty hanya dibuat 1 kali saja.

Method afterTestClass() yang diberi annotation @AfterClass akan dikerjakan setelah seluruh test case pada object tersebut selesai dikerjakan.  Saya tidak melakukan apa-apa disini.  Saya tidak menutup database dan server Jetty, karena mereka masih diperlukan untuk class berikutnya.  Saya tidak tahu class mana yang dikerjakan terakhir kali.  Lalu kapan database dan server Jetty akan ditutup?  Pada saat pengujian berakhir dimana  JVM JUnit dimatikan, segala sesuatu yang dibuat oleh JVM  termasuk database dan server akan dibuang dari memory secara otomatis.

Saya masih perlu membuat sebuah class lagi, yaitu annotation DataSets.  Saya melihat trik memakai DataSets ini di buku JUnit in Action (2nd edition) dan buku Spring 3 Pro.  DataSets adalah sebuah annotation yang berisi informasi file Excel apa yang akan di-load ke database oleh DbUnit.  Setiap sheet di Excel mewakili sebuah tabel, setiap kolom dalam sheet mewakili field di tabel, dan setiap baris dalam sheet mewakili record di tabel.  Pada kedua buku tersebut, DataSets akan diperiksa untuk setiap method.  Akan tetapi karena saya tidak menggunakan SpringJUnit4ClassRunner seperti di buku, saya tidak bisa memeriksa per-method.  Jalan terbaik yang bisa saya lakukan adalah memeriksa per-class.  Berikut ini adalah isi annotation DataSets saya:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSets {

	String setUpDataSet() default "";

}

Sekarang, aplikasi saya bisa berjalan pada 2 container, yaitu secara normal di tc Server (Tomcat 6) dan selama pengujian selenium di Jetty.  Jika aplikasi saya berjalan di tc Server, ia akan mengakses database MySQL.  Jika aplikasi saya berjalan di Jetty, maka ia akan mengakses database in-memory H2.  Hal ini dapat diatur dengan mudah berkat Spring profiles.  Berikut ini adalah konfigurasi Spring saya:

...
<bean id="parentJpaProperties" abstract="true" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
 <property name="properties">
   <props>
     <prop key="hibernate.max_fetch_depth">3</prop>
     <prop key="hibernate.jdbc.fetch_size">50</prop>
     <prop key="hibernate.jdbc.batch_size">10</prop>
     <prop key="hibernate.id.new_generator_mappings">true</prop>
     <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
   </props>
 </property>
</bean>

<beans profile="dev,production">
 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" >
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="jdbc:mysql://localhost:3306/database" />
   <property name="username" value="user" />
   <property name="password" value="password" />
 </bean>

 <bean id="jpaProperties" parent="parentJpaProperties">
   <property name="properties">
     <props merge="true">
       <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
     </props>
   </property>
 </bean>
</beans>

<beans profile="selenium-test">
 <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/testdatabase" />

 <bean id="jpaProperties" parent="parentJpaProperties">
   <property name="properties">
     <props merge="true">
       <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
       <prop key="hibernate.hbm2ddl.auto">create-drop</prop> 
     </props>
   </property>
 </bean>
</beans>
...

Informasi mengenai deklarasi inheritance yang saya pakai dapat dilihat di artikel Memakai Ulang Props Di Definisi XML Spring.  Yang jelas, pada saat aplikasi dijalankan dalam container Jetty, Spring profile yang aktif haruslah selenium-test.

Pertanyaan bagaimana membuat Spring profile selenium-test menjadi aktif saat aplikasi dijalankan dalam container Jetty?  Informasi lebih lanjut dapat dilihat di artikel Mengatur Spring Profile Untuk Embedded Jetty.   Berikut ini adalah isi file override-web.xml saya adalah:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

  <context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>selenium-test</param-value>
  </context-param>

  <resource-ref>
    <res-ref-name>jdbc/testdatabase</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
  </resource-ref>

</web-app>

Setelah ini, saya bisa membuat file Excel yang berisi data yang akan dimasukkan ke database secara otomatis setiap kali sebuah method di test case dikerjakan.  File tersebut juga diletakkan di src/test/resources.  Pada akhirnya, isi folder tersebut akan terlihat seperti (file log4j.xml dan StressTest.jmx tidak termasuk dalam pembahasan di tulisan ini):

Isi folder src/test/resources

Isi folder src/test/resources

Terakhir, saya tinggal membuat class  yang memakai Selenium untuk menguji tampilan.  Berikut ini adalah contoh test case yang saya buat:

import static org.junit.Assert.*;
import org.dbunit.dataset.ITable;
import org.junit.Test;

@DataSets(setUpDataSet="/dataNormal.xls")
public class AkunSayaTest extends AbstractSeleniumTest {
  @Test
  public void testUpdateProfilUntukKaryawan() throws Exception {

  // Login Sebagai User PEGAWAI
  selenium.open("/proyek/");
  selenium.waitForPageToLoad("30000");
  selenium.click("link=Login");
  selenium.waitForPageToLoad("30000");
  selenium.type("id=login_user", "lena@gmail.com");
  selenium.type("id=login_pass", "lenalovejocki");
  selenium.click("name=login");
  selenium.waitForPageToLoad("30000");

  // Membuka Menu Akun Saya
  selenium.open("/proyek/#&panel1-2");
  selenium.click("link=Akun Saya");
  selenium.waitForPageToLoad("30000");

  // Men-klik menu Edit Profil
  selenium.click("id=editProfil");
  selenium.waitForCondition("JL.dialogUtama['dialogUser'].is(':visible')==true", "1000");

  // Memastikan tampilan di dialog benar
  assertEquals("lena@gmail.com", selenium.getValue("id=email"));
  // .. dan sebagainya ...

  // Melakukan perubahan data		
  selenium.type("id=nama", "Jona Junior");
  selenium.click("id=frmUserbtnSimpan");
  selenium.waitForCondition("JL.dialogUtama['dialogUser'].is(':visible')==false", "1000");

  // Memastikan data sudah tersimpan di database
  assertEquals("Jona Junior", dataTester.getConnection().
     createQueryTable("user", "select * from user where email='lena@gmail.com'").getValue(0, "nama"));

  // Kembali membuka menu Edit Profile
  selenium.click("id=editProfil");
  selenium.waitForCondition("JL.dialogUtama['dialogUser'].is(':visible')==true", "1000");

  // Memastikan tampilan di dialog benar
  assertEquals("lena@gmail.com", selenium.getValue("id=email"));
  // .. dan sebagainya ..
 }
}

Kode program pengujian front-end saya sekarang bisa lebih singkat dan lebih mudah dipahami.  Selain itu , kini saya  tidak perlu khawatir lagi untuk menambah/mengubah data sebelum dan setelah pengujian.

Mengatur Spring Profile Untuk Embedded Jetty

Pada saat menjalankan aplikasi di dalam embedded Jetty dengan tujuan pengujian, kadang-kadang saya perlu mengubah profile Spring yang sedang aktif, misalnya dari dev menjadi test.  Sebagai contoh, ini adalah potongan web.xml aplikasi yang saya buat:

<context-param>
  <param-name>spring.profiles.active</param-name>
  <param-value>dev</param-value>
</context-param>

Pada saat menjalankan embedded Jetty, saya memberikan referensi ke web.xml ini, misalnya seperti berikut:

WebAppContext context = new WebAppContext();
context.setDescriptor("../FolderProyek/src/main/webapp/WEB-INF/web.xml");

Pertanyaannya adalah bagaimana mengubah nilai param spring.profiles.active dari dev menjadi test secara otomatis.  Yup, otomatis, karena tidak mungkin saya bisa dipaksa mengedit file web.xml secara manual setiap kali melakukan pengujian front-end (menjadi test) dan setiap kali menjalankan aplikasi secara normal (menjadi dev).

Sekilas terbersit bahwa Spring bisa membaca nilai profile yang aktif dari environment variable, sehingga harusnya saya bisa menambahkan System.setProperty(“spring.profiles.active”, “test”);  pada kode program di atas.  Sayangnya, hal seperti ini TIDAK bisa dilakukan, karena nilai pada web.xml yang dibaca akan MENIMPA nilai environment variable!!!

Lalu apa solusinya?  Saya harus memberitahukan pada Jetty bahwa nilai pada web.xml harus ditimpa oleh sesuatu nilai lain, misalnya dengan kode program berikut ini:

List<String> listOverrideDescriptors = new ArrayList<String>();
listOverrideDescriptors.add("override-web.xml");
context.setOverrideDescriptors(listOverrideDescriptors);

Pada kode program tersebut, saya harus menyediakan sebuah file override-web.xml yang berada dalam format yang sama seperti web.xml.  Bedanya, override-web.xml ini tidak perlu berisi seluruh definisi web.xml secara lengkap, melainkan hanya perlu menyertakan bagian yang akan diubah.  Sebagai contoh, untuk mengubah profile di Spring, isi override-web.xml saya adalah:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

  <context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>test</param-value>
  </context-param>

</web-app>

Memakai Ulang Props Di Definisi XML Spring

Sejak Spring versi 3.1, terdapat fungsi profiles dimana definisi bean tertentu bisa diatur agar hanya dikerjakan (dianggap ada) bila kondisi tertentu dipenuhi.  Kondisi yang dimaksud disini adalah nama profile harus sesuai.   Nama profile bisa ditentukan melalui VM arguments saat menjalankan server atau melalui tag <param-name> di <context-param> web.xml.  Cukup berikan parameter spring.profiles.active dengan nilai apa saja yang mewakili nama profile.  Btw, nama profile juga bisa ditentukan melalui annotation @ActiveProfiles bila dipakai dalam pengujian melalui SpringJUnit4ClassRunner.class.

Saya sering memakai fitur profile untuk membedakan bean untuk pengujian dan produksi.  Misalnya, pada pengujian, saya akan mendefinisikan database H2 (embedded)  sementara pada produksi, saya akan memakai database MySQL.  Selain itu, saya juga dapat mengatur agar Hibernate JPA  secara otomatis create-drop tabel hanya pada profile pengujian.

Tapi penggunaan profile menimbulkan sebuah permasalahan baru yang berkaitan dengan copy paste.  Sebagai contoh berikut ini adalah potongan dari definisi XML untuk bean saya:

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="jpaVendorAdapter">
    <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
  </property>
  <property name="packagesToScan" value="co.id.jocki.latihan.domain" />
  <property name="jpaProperties" ref="jpaProperties" />
</bean>

<beans profile="dev,production">

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/latihan" />
    <property name="username" value="user" />
    <property name="password" value="skripsi" />
  </bean>

  <bean id="jpaProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="properties">
      <props>
        <prop key="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
        <prop key="hibernate.max_fetch_depth">3</prop>
        <prop key="hibernate.jdbc.fetch_size">50</prop>
        <prop key="hibernate.jdbc.batch_size">10</prop>
        <prop key="hibernate.id.new_generator_mappings">true</prop>
        <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
      </props>
    </property>
  </bean>

</beans>

<beans profile="test">

  <jdbc:embedded-database id="dataSource" type="H2" />

  <bean id="jpaProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="properties">
      <props>
        <prop key="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
        <prop key="hibernate.max_fetch_depth">3</prop>
        <prop key="hibernate.jdbc.fetch_size">50</prop>
        <prop key="hibernate.jdbc.batch_size">10</prop>
        <prop key="hibernate.hbm2dll.auto">create-drop</prop>
        <prop key="hibernate.id.new_generator_mappings">true</prop>
        <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
      </props>
    </property>
  </bean>

</beans>

<beans profile="selenium-test">

  <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/testdatabase" />

  <bean id="jpaProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="properties">
      <props>
        <prop key="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
        <prop key="hibernate.max_fetch_depth">3</prop>
        <prop key="hibernate.jdbc.fetch_size">50</prop>
        <prop key="hibernate.jdbc.batch_size">10</prop>
        <prop key="hibernate.hbm2dll.auto">create-drop</prop>
        <prop key="hibernate.id.new_generator_mappings">true</prop>
        <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
      </props>
    </property>
  </bean>

</beans>

Pada definisi bean di atas, bean jpaProperties berisi properties untuk Hibernate.  Pada profile dev dan production, saya memakai MySQL sehingga nilai hibernate.dialect adalah org.hibernate.dialect.MySQLDialect.  Saya tidak bisa memakai ulang bean ini di profile test dan selenium-test, karena pada profile tersebut saya memakai database H2 sehingga nilai hibernate.dialect seharusnya adalah org.hibernate.dialect.H2Dialect.

Permasalahan akan timbul bila suatu saat nanti saya merubah nilai jpaProperties.  Mungkin saya mengubah nilai baru, atau menghapus.  Seandainya saya terburu-buru dan sedang dilanda pusing (karena developer sangat sering coding dalam keadaan seolah-olah dikejar binatang buas), maka mungkin sekali saya LUPA kalau saya harus mengubah jpaProperties di TIGA tempat yang berbeda!

Ok, karena sekarang saya sedang santai, saya harus mempersiapkan perisai untuk mengurangi kemungkinan hal tidak diinginkan di masa depan.

Nilai-nilai yang umum dari ketiga bean jpaProperties tersebut dapat saya taruh ke sebuah bean tersendiri sebagai parent.  Dengan demikian, pada saat saya akan melakukan perubahan secara umum, saya hanya perlu mengubah satu bean parent  ini.  Berikut ini adalah definisi bean tersebut:

<bean id="parentJpaProperties" abstract="true" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
  <property name="properties">
    <props>
       <prop key="hibernate.max_fetch_depth">3</prop>
       <prop key="hibernate.jdbc.fetch_size">50</prop>
       <prop key="hibernate.jdbc.batch_size">10</prop>
       <prop key="hibernate.id.new_generator_mappings">true</prop>
       <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
    </props>
  </property>
</bean>

Yang perlu diperhatikan adalah saya menambahkan abstract=”true” pada definisi bean parent tersebut.  Ini berarti bean parentJpaProperties tidak dapat dipakai secara langsung, melainkan harus melalui child-nya.

Pada profile untuk dev dan production, saya bisa membuat bean jpaProperties seperti berikut ini:

<bean id="jpaProperties" parent="parentJpaProperties">
  <property name="properties">
    <props merge="true">
      <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
    </props>
  </property>
</bean>

Pada bean ini saya mendefinisikan parent=”parentJpaProperties” sehingga nilai dari bean parentJpaProperties bisa dipakai ulang.  Saya menambahkan merge=”true” sehingga props yang ada disini akan digabungkan dengan milik parent, bukannya menimpa dan menggantikan milik parent.

Pada profile test dan selenium-test, saya tinggal membuat bean dengan nama yang sama, tetapi isi props yang berbeda, seperti berikut ini:

<bean id="jpaProperties" parent="parentJpaProperties">
  <property name="properties">
    <props merge="true">
      <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
      <prop key="hibernate.hbm2dll.auto">create-drop</prop>
    </props>
  </property>
</bean>

‘Kebanyakan’ memakai context:component-scan di Spring MVC + Spring Core

Jika saya membuat proyek baru dengan memakai template Spring MVC dari STS, maka secara otomatis akan dibuatkan dua file konfigurasi Spring, yaitu:

  1. /src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml
  2. /src/main/webapp/WEB-INF/spring/root-context.xml

Jika saya membuka definisi DispatcherServlet di web.xml, saya akan menemukan parameter seperti berikut ini:

<servlet>
  <servlet-name>appServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
  </init-param>
</servlet>

Terlihat bahwa file konfigurasi servlet-context.xml akan dipakai oleh DispatcherServlet.

Dengan pemikiran yang naif, saya awalnya menganggap bahwa root-context.xml akan dipanggil secara tidak langsung oleh servlet-context.xml sehingga isinya akan digabungkan ke dalam 1 context.  Tapi ternyata saya salah!!!

root-context.xml dan servlet-context.xml akan dipakai untuk membuat dua context yang berdiri sendiri!!  Masing-masing isi-nya berdiri sendiri..

Lalu, apa konsekuensinya?  Biasanya, pada servlet-context.xml, saya mendefinisikan hal-hal yang berhubungan dengan Spring Web MVC.  Sementara pada root-context.xml, saya akan menyertakan file konfigurasi lain yang berhubungan dengan JPA, Spring Security, dan sebagainya.

Pada servlet-context.xml yang naif, saya mengisyaratkan Spring Core agar membuat bean secara otomatis dengan mencari annotation di package yang saya tentukan:

<context:component-scan base-package="com.lena.aplikasi" />

Demikian juga, pada root-context.xml, saya perlu menentukan class-class yang akan di-scan, dengan konfigurasi seperti:

<context:component-scan base-package="com.lena.aplikasi" />

Secara naif, saya mengira setiap class yang memiliki annotation yang diturunkan dari @Component akan dibuat menjadi singleton dalam 1 context yang sama.  Tapi ternyata tidak!  Masing-masing class akan dibuat menjadi singleton dalam 2 context yang berbeda.  Untuk setiap class komponen, akan ada 1 singleton di context servlet-context.xml dan 1 singleton lagi di context root-context.xml.

Untuk membuktikannya, saya mencari di log console saat menjalankan aplikasi.  Saya akan melihat ada DUA baris terpisah yang mengandung nama bean kompoen saya, yaitu:

Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@80e8fe: ... [daftar bean]

dan

Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1b6c585: ... [daftar bean]

Apakah ada masalah jika diteruskan seperti ini?  Tentu saja.  Membuat definisi bean dua kali berarti duplikasi penggunaan memori.  Selain itu, waktu startup akan menjadi lebih lama.  Dan, bisa jadi akan muncul bug-bug suatu hari nanti.   Saya  menemukan bug aneh seperti ini karena pada saat melakukan integration testing pada service layer dengan  JUnit, saya hanya memakai  root-context.xml saja.  Sementara ternyata, pada saat aplikasi dijalankan sepenuhnya (dengan servlet-context.xml dan root-context.xml), terdapat perilaku yang berbeda.

Lalu bagaimana solusinya?  Saya mulai membiasakan diri hanya menyertakan bean/komponen yang berhubungan dengan web di servlet-context.xml.  Karena biasanya class-class yang berhubungan dengan web berada di subpackage web, maka saya mendefinisikan ulang component-scan di servlet-context.xml menjadi:

<context:component-scan base-package="com.lena.aplikasi.web" />

Sementara itu, pada root-context.xml, saya akan menyertakan sisa-nya.  Tapi karena komponen-komponen lain tersebut bisa saja terpencar di berbagai subpackage yang berbeda-beda, maka  root-context.xml saya tetap mencari seluruh class yang ada, tetapi kali ini mengabaikan yang ada di subpackage web:

<context:component-scan base-package="com.lena.aplikasi">
   <context:exclude-filter type="regex" expression="com\.lena\.aplikasi\.web\..*" />
</context:component-scan>

Memakai MessageConverter di Spring 3.1 Untuk Web Services REST

Salah satu fitur menarik yang diperkenalkan oleh Spring Framework 3.1 adalah message converter yang dalam bentuk tag <mvc:message-converters>.  Dengan message converter, saya bisa membuat representasi  object di aplikasi saya dengan mudah dalam bentuk JSON dan XML secara otomatis.   Untuk mendukung konversi objek ke/dari JSON, saya memakai Jackson JSON library (http://jackson.codehaus.org).  Sementara untuk konversi objek ke/dari XML, saya memakai Castor (http://castor.codehaus.org).

Saya mulai dengan membuat sebuah proyek baru di STS, dengan memakai template Spring MVC Project.   Sebelum mulai membuat kode program, saya menambahkan dependencies Maven ke proyek saya seperti yang terlihat pada gambar berikut ini:

Dependencies Maven

Dependencies Maven

Kemudian saya membuat sebuah class dengan nama Mahasiswa di package co.id.jocki.domain.  Isi dari class Mahasiswa adalah:

package co.id.jocki.domain;

import java.io.Serializable;

public class Mahasiswa implements Serializable {

	private static final long serialVersionUID = 225855015823197676L;

	private String nim;
	private String nama;
	private int usia;

	public String getNim() {
		return nim;
	}
	public void setNim(String nim) {
		this.nim = nim;
	}
	public String getNama() {
		return nama;
	}
	public void setNama(String nama) {
		this.nama = nama;
	}
	public int getUsia() {
		return usia;
	}
	public void setUsia(int usia) {
		this.usia = usia;
	}	

}

Lalu, pada package co.id.jocki, saya membuat sebuah class bernama LatihanController.  Isi dari class tersebut adalah:

package co.id.jocki;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import co.id.jocki.domain.Mahasiswa;

@Controller
@RequestMapping(value="/mahasiswa")
public class LatihanController {

	private Mahasiswa mahasiswa;

	@RequestMapping(value="/listdata", method=RequestMethod.GET)
	@ResponseBody
	public Mahasiswa listData() {
		if (mahasiswa==null) {
			mahasiswa = new Mahasiswa();
			mahasiswa.setNama("Makdalena Hendry");
			mahasiswa.setNim("99999999");
			mahasiswa.setUsia(21);
		}
		return mahasiswa;
	}

}

Pada kasus nyata, tentu saja isi controller tidak sesederhana ini (misalnya masih ada get, delete, update, dsb).  Object yang ada juga tidak dibuat disini, melainkan seharusnya diambil dari medium penyimpanan (misalnya database).

Yang menarik disini adalah saya  tidak melakukan proses transformasi ke JSON ataupun XML secara manual.  Saya juga tidak memanggil sebuah fungsi ajaib.  Saya hanya mengembalikan sebuah objek mahasiswa seperti biasanya layaknya kode program standard.

Lalu bagaimana konversi bisa dilakukan?  Karena saya memberitahukannya secara deklaratif (tanpa kode program) dengan mengedit file servlet-context.xml.  File ini dapat ditemukan di lokasi src/main/webapp/WEB-INF/spring/appServlet.  Saya mengubah file tersebut sehingga isinya menjadi:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

  <mvc:annotation-driven>
    <mvc:message-converters>
      <beans:bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
      <beans:bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
        <beans:property name="marshaller" ref="castorMarshaller"/>
        <beans:property name="unmarshaller" ref="castorMarshaller"/>
      </beans:bean>
    </mvc:message-converters>
  </mvc:annotation-driven>

  <beans:bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
    <beans:property name="mappingLocation" value="classpath:oxm-mapping.xml"/>
  </beans:bean>

  <context:component-scan base-package="co.id.jocki" />

</beans:beans>

Rahasianya terletak di <mvc:message-converters> dimana saya mendeklarasikan bean dari class MappingJacksonHttpMessageConverter dan class MarshallingHttpMessageConverter.  Khusus untuk yang XML, saya perlu membuat file oxm-mapping.xml (nama yang sama seperti di property mappingLocation di bean castorMarshaller.

Saya akan membuat file oxm-mapping.xml ini di folder src/main/resources.  Isi file tersebut menentukan bagaimana memetakan sebuah class Java ke XML (dan sebaliknya) seperti yang terlihat di berikut ini:

<?xml version="1.0" encoding="UTF-8"?>
<mapping xmlns="http://castor.exolab.org/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://castor.exolab.org/ http://castor.org/mapping.xsd">

  <class name="co.id.jocki.domain.Mahasiswa">
    <map-to xml="mahasiswa" ns-uri="http://jockihendry.com/mahasiswa/"
       ns-prefix="mahasiswa"/>

    <field name="nim" type="string" >
      <bind-xml auto-naming="deriveByField" node="element"/>
    </field>

    <field name="nama" type="string">
      <bind-xml auto-naming="deriveByField" node="element" />
    </field>

    <field name="usia" type="integer">
      <bind-xml auto-naming="deriveByField" node="element" />
    </field>
  </class>

</mapping>

Setelah ini, saya  menjalankan tc Server untuk menguji web service REST tersebut.

Untuk melakukan pengujian, saya akan menggunakan cURL (http://curl.haxx.se), sebuah tools command-line yang bisa dipakai untuk browsing berbasis teks.  Karena saya pernah meng-install Zend Studio, tools tersebut secara otomatis sudah ada dan siap dipakai.

Saya mulai dengan memanggil halaman http://localhost:8080/latihan-rest/mahasiswa/listdata.  Pada kode program, terlihat controller hanya mengembalikan sebuah objek mahasiswa.  Tetapi saya menginginkan kembalian berupa JSON.  Dan, server saya ternyata sudah mendukungnya seperti yang terlihat di tampilan berikut:

Komunikasi REST dengan JSON

Komunikasi REST dengan JSON

Lalu, kali ini saya menginginkan kembalian berupa XML.  Dan sekali lagi, server saya secara otomatis sudah mendukungnya seperti yang terlihat di gambar berikut:

Komunikasi REST dengan XML

Komunikasi REST dengan XML

Memakai Spring MVC di SpringSource Tool Suite

MVC adalah sebuah design pattern yang memisahkan logika aplikasi ke dalam tiga wilayah yang berbeda yaitu Model, View, dan Controller.  Hal ini berbeda dengan multi-tier (N-tier) dimana pada N-tier terdapat pemisahan tier pada mesin yang berbeda di komputer yang berbeda.  Salah satu alasan terbentuknya arsitektur N-tier adalah untuk menciptakan skalabilitas yang lebih baik.  Sebagai contoh, program database umumnya membutuhkan memori dalam jumlah besar sementara program logika membutuhkan prosesor yang kencang.  Dengan arsitektur N-tier, database berada di sebuah tier tersendiri yang diletakkan pada server dengan memori berjumlah besar dan prosesor sedang.  Sementara itu, business logic dapat berada di sebuah server dengan memori sedang tetapi memiliki prosesor berkecepatan tinggi.

Bila N-tier tercipta karena alasan skalabilitas, maka MVC terlahir untuk memisahkan program kita kedalam tiga elemen yang terpisah dan sebisa mungkin tidak saling terkait. Sebagai contoh, bagian View yang tadinya adalah HTML, dapat diganti menjadi PDF, Flash, ataupun WAP (mobile), tanpa harus memprogram ulang Controller dan Model.  Bila kita ingin meletakkan MVC ke dalam N-tier, maka MVC berada di Presentation Tier.

Pada artikel ini, saya akan membuat sebuah aplikasi MVC sederhana dengan memakai Spring MVC (bagian dari Spring Framework) dan IDE SpringSource Tool Suite (STS).  Spring Framework memang lebih terkenal dengan IoC container-nya, yang banyak ditiru di JEE 6.  Memakai framework ini menimbulkan sebuah perasaan nostalgia tersendiri, karena Spring Framework adalah framework Java yang pertama kali saya pakai setelah lulus kuliah dan bekerja sebagai programmer junior.

Pada STS, saya mulai dengan memilih menu File, New, dan Spring Template Project.  Pada kotak dialog New Template Project, saya memilih Spring MVC Project.  Pada dialog New Spring MVC Project yang muncul, saya mengisi Project Name dengan latihan-mvc, mengisi top-level package dengan co.id.jocki.latihan dan kemudian men-klik tombol Finish.  STS akan membuat struktur proyek seperti pada gambar berikut ini:

Struktur Proyek Spring MVC

Struktur Proyek Spring MVC

Template Spring MVC secara otomatis membuat sebuah controller bernama HomeController dan sebuah view dengan nama home.  Saya akan mengabaikan dua file ini.

Membuat Model

Pada langkah berikutnya, saya membuat sebuah model dengan nama Mahasiswa di package co.id.jocki.mahasiswa.  Saya mulai dengan men-klik kanan pada src/main/java kemudian memilih New, Class.  Lalu saya mengisi dialog New Java Class dengan seperti yang terlihat di gambar:

Membuat Model Baru

Membuat Model Baru

Class Mahasiswa sebagai model memiliki isi seperti berikut ini:

package co.id.jocki.mahasiswa;

public class Mahasiswa {

  private String nim;
  private String nama;
  private Date tanggalLahir;

  public String getNim() {
    return nim;
  }
  public void setNim(String nim) {
    this.nim = nim;
  }
  public String getNama() {
    return nama;
  }
  public void setNama(String nama) {
    this.nama = nama;
  }
  public Date getTanggalLahir() {
    return tanggalLahir;
  }
  public void setTanggalLahir(Date tanggalLahir) {
    this.tanggalLahir = tanggalLahir;
  }
}

Sebelum beranjak meninggalkan model ini, saya ingin mencoba menggunakan fitur Bean Validation yang didukung oleh Spring Framework.  Dengan Bean Validation, saya tidak perlu report-repot melakukan validasi di sisi client maupun di sisi server, karena semuanya akan dilakukan otomatis oleh Spring Framework.  Untuk itu, saya perlu menambahkan dependency baru di Maven dengan melakukan langkah-langkah seperti berikut ini:

  1. Buka file pom.xml di STS.  Akan muncul sebuah halaman editor untuk Maven.
  2. Buka tab Dependencies (disebelah kanan tab Overview).
  3. Klik tombol Add…
  4. Pada bagian Enter groupId, artifactId or sha1 prefix or pattern, isi dengan validation-api.
  5. Klik pada javax.validationseperti yang terlihat di gambar berikut ini:

    Dependency Maven untuk validation-api

    Dependency Maven untuk validation-api

  6. Klik tombol OK.
  7. Kembali Klik tombol Add…
  8. Pada bagian Enter groupId, artifactId or sha1 prefix or pattern, isi dengan hibernate-validator.
  9. Klik pada org.hibernateseperti yang terlihat di gambar berikut ini:

    Dependency Maven untuk hibernate-validator

    Dependency Maven untuk hibernate-validator

  10. Klik tombol OK.
  11. Kembali klik tombol Add…
  12. Pada bagian Enter groupId, artifactId or sha1 prefix or pattern, isi dengan joda-time.
  13. Klik pada joda-timeseperti yang terlihat di gambar berikut ini:

    Dependency Maven untuk joda-time

    Dependency Maven untuk joda-time

  14. Klik tombol OK.
  15. Klik kanan pada nama proyek, pilih Maven, Update Dependencies… dan klik pada tombol OK. Beri tanda centang pada Force Update of Snapshots/Releases.  Jangan lupa menghubungkan komputer ke internet agar Maven dapat men-download JAR yang dibutuhkan.  Hibernate Validator dan Joda Time adalah dua framework terpisah yang bukan merupakan bagian dari Spring Framework.

Sekarang, kembali ke class Mahasiswa.java, saya akan mengubah isi file tersebut menjadi:

package co.id.jocki.mahasiswa;

import java.util.Date;

import javax.validation.constraints.Past;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.format.annotation.DateTimeFormat;

public class Mahasiswa {

    @Size(min=8, max=8, message="NIM harus 8 digit!")
    private String nim;

    @NotEmpty(message="Nama harus di-isi!")
    private String nama;

    @Past(message="Tanggal harus berada di masa lalu!")    
    @DateTimeFormat(pattern="dd/MM/YYYY")    
    private Date tanggalLahir;

    public String getNim() {
        return nim;
    }
    public void setNim(String nim) {
        this.nim = nim;
    }
    public String getNama() {
        return nama;
    }
    public void setNama(String nama) {
        this.nama = nama;
    }
    public Date getTanggalLahir() {
        return tanggalLahir;
    }
    public void setTanggalLahir(Date tanggalLahir) {
        this.tanggalLahir = tanggalLahir;
    }   
}

Pada kode program di atas, saya memberikan validasi @Size pada atribut nim dimana panjang nim harus 8 karakter.  Pesan yang ada di message akan ditampilkan di view bila nilai tidak memenuhi persyaratan.  Saya juga memberikan validasi @NotEmpty untuk atribute nama, sehingga nantinya nama wajib di-isi.  Saya memberikan validasi @Past di tanggalLahir untuk memastikan bahwa tanggalLahir yang dimasukkan oleh pengguna tidak melewati batas hari ini (karena tidak mungkin ada mahasiswa yang tanggal lahirnya tahun depan tetapi sudah duluan mendaftar).

Membuat View

Sekarang, saya akan membuat View.  Pada src/main/webapp/WEB-INF/views, saya men-klik kanan folder views dan memilih New, Others.  Pada dialog yang muncul, saya memilih Web, JSP File, kemudian men-klik tombol Next.  Pada File name, saya mengisi dengan nama tambahMahasiswa.jsp.  Setelah itu, saya men-klik tombol Finish.  Struktur proyek akan terlihat seperti pada gambar berikut ini:

Menambah view baru

Menambah view baru

Berikut ini adalah isi file tambahMahasiswa.jsp:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<style type="text/css">
    .error {color: red; }    
</style>
<title>Tambah Mahasiswa Baru</title>
</head>
<body>
    <h1>Tambah Mahasiswa Baru</h1>    
    <form:form modelAttribute="mahasiswa" method="post">
        <fieldset>    
        <p>
            <form:label path="nim" for="nim" cssErrorClass="error">NIM:</form:label>
            <form:input path="nim" />
            <form:errors path="nim" cssClass="error"/>
        </p>

        <p>
            <form:label path="nama" for="nama" cssErrorClass="error">Nama:</form:label>
            <form:input path="nama" />
            <form:errors path="nama" cssClass="error"/>
        </p>

        <p>
            <form:label path="tanggalLahir" for="tanggalLahir" cssErrorClass="error">Tanggal Lahir:</form:label>
            <form:input path="tanggalLahir"/>
            <form:errors path="tanggalLahir" cssClass="error"/>
        </p>
        <p>
            <input type="submit"/>
        </p>        
        </fieldset>
    </form:form>
</body>
</html>

Pada halaman JSP tersebut, saya memakai tag library yang disediakan oleh Spring Framework dengan memakai directive taglib.  Spring MVC menyediakan tag seperti <form:form>, <form:label>, <form:input> dan <form:errors> untuk mempermudah data-binding dengan model.  Pada JSP tersebut, saya melakukan binding dengan model Mahasiswa.  Nilai path di <form:label>, <form:input> dan <form:errors> harus sesuai dengan nama atribut/variabel di model. Pada <form:label> saya menambahkan cssErrorClass sehingga bila terjadi kesalahan validasi, maka class tersebut akan dipakai (pada contoh ini, mengubah warna tulisan menjadi merah).

Kemudian, dengan mengikuti langkah yang sama dengan yang diatas, saya membuat sebuah view baru dengan nama lihatMahasiswa.jsp.  Isi dari file lihatMahasiswa.jsp adalah:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>   
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<style type="text/css">
    table {margin-bottom: 20px; border: solid 1px black;}
    th {text-align: left; padding-right: 20px; border-bottom: solid 2px black;}
    td {padding-right: 20px;}    
</style>
<title>Lihat Daftar Mahasiswa</title>
</head>
<body>
<h1>Daftar Mahasiswa</h1>
<table>
<thead>
    <tr><th>NIM</th><th>Nama</th><th>Tanggal Lahir</th></tr>
</thead>
<c:forEach items="${lstMahasiswa}" var="mahasiswa">
    <tr>
    <td>${mahasiswa.nim}</td>
    <td>${mahasiswa.nama}</td>
    <td><fmt:formatDate value="${mahasiswa.tanggalLahir}" pattern="dd/MM/yyyy"/></td>
    </tr>
</c:forEach>
</table>
<a href="mahasiswa">Tambah Mahasiswa Baru</a>
</body>
</html>

Pada halaman JSP ini, saya mengharapkan controller untuk mengirimkan sebuah List dengan nama lstMahasiswa.  List tersebut harus merupakan kumpulan dari class Mahasiswa (model yang saya pakai).  Lalu dengan menggunakan JSTL <c:forEach>, saya melakukan perulangan untuk menampilkan setiap class Mahasiswa sebagai baris di dalam tabel.  Saya juga menggunakan JSTL <fmt:formatDate> untuk men-format atribut tanggalLahir yang bertipe java.util.Date.

Membuat Controller

Berikutnya, saya membuat sebuah controller dengan nama MahasiswaController.java di package yang sama.  Caranya adalah dengan men-klik kanan di nama package co.id.jocki.mahasiswa, kemudian memilih menu New, Class.  Pada dialog New Java Class yang muncul, isi nama dengan MahasiswaController, kemudian klik tombol Finish.  Kemudian, saya membuat kode program seperti berikut ini untuk MahasiswaController.java:

package co.id.jocki.mahasiswa;

import java.util.ArrayList;
import java.util.List;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value="/mahasiswa")
public class MahasiswaController {

    private List<Mahasiswa> lstMahasiswa = new ArrayList<Mahasiswa>();

    @RequestMapping(method=RequestMethod.GET)
    public String getFormTambah(Model model) {
        model.addAttribute(new Mahasiswa());
        return "tambahMahasiswa";
    }    

    @RequestMapping(method=RequestMethod.POST)
    public String tambahMahasiswa(@Valid Mahasiswa mahasiswa, BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "tambahMahasiswa";
        }
        lstMahasiswa.add(mahasiswa);
        model.addAttribute("lstMahasiswa", lstMahasiswa);
        return "lihatMahasiswa";
    }
}

Untuk menjalankan proyek ini, klik pada icon Run As… dan pilih Run As, Run on Server.  Secara default, STS akan memakai server VMware vFabric tc Server Developer Edition v2.7 seperti yang terlihat digambar berikut ini:

Menjalankan Proyek

Menjalankan Proyek

Pada dasarnya tc Server adalah sebuah server Apache Tomcat yang dilengkapi fitur tambahan (dan dukungan komersial).  Pada kotak dialog yang muncul, beri centang pada Always use this server when running this project.  Klik tombol Next.  Pastikan bahwa latihan-mvc berada di daftar configured, bukan di available.  Setelah itu klik tombol Finish.

STS akan berusaha menjalankan tc Server sembari menampilkan informasi di window Console.  Bila tc Server sudah selesai dijalankan, akan terdapat baris dengan tulisan seperti:

INFO: Server startup in 2789 ms

Saya akan mulai dengan membuka browser dan memasukkan URL http://localhost:8080/latihan-mvc/mahasiswa.  Hal ini akan menyebabkan salah satu dari method di MahasiswaController dikerjakan, karena nilai value di @RequestMapping untuk MahasiswaController adalah /mahasiswa.

Karena saya memasukkan URL secara biasa dengan mengetik di browser, maka request method yang saya pakai adalah request method GET.  Dengan demikian, getFormTambah() di MahasiswaController akan dikerjakan.  getFormTambah() hanya membuat sebuah objek Mahasiswa baru, kemudian mengembalikan sebuah String tambahMahasiswa.  Hal ini akan menyebabkan view tambahMahasiswa.jsp ditampilkan di browser, seperti yang terlihat di gambar berikut ini:

View tambahMahasiswa

View tambahMahasiswa

Pada saat saya menekan tombol Submit Query, maka URL /mahasiswa akan kembali dipanggil, tapi kali ini dengan request method POST.  Hal ini menyebabkan method tambahMahasiswa() di MahasiswaController dikerjakan.  Saya menambahkan annotation @Valid pada model Mahasiswa sehingga Spring MVC akan melakukan validasi model yang dikirim oleh view secara otomatis seperti yang terlihat pada gambar berikut ini:

Validasi secara otomatis

Validasi secara otomatis

Bila tidak ada kesalahan, maka model yang dikirim oleh view akan disimpan ke dalam sebuah List.  Tentu saja, pada proyek nyata, model tidak disimpan ke dalam list melainkan kedalam sebuah database.  Setelah menyimpan model ke dalam List, tambahMahasiswa() akan memanggil view lihatMahasiswa.jsp sembari mengirimkan List berisi model Mahasiswa dengan nama atribut lstMahasiswa, sehingga tampilan yang diperoleh adalah seperti berikut ini:

Tampilan view lihatMahasiswa

Tampilan view lihatMahasiswa

Untuk melihat gambaran umum MVC di proyek secara garis besar, saya dapat membuka window Spring Explorer yang terlihat seperti pada gambar berikut ini:

Tampilan Spring Explorer

Tampilan Spring Explorer