Belajar Memakai Specification Di Spring Data JPA

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

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

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

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

Tabel dengan fasilitas filtering per kolom

Tabel dengan fasilitas filtering per kolom

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

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

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

  public enum Operation { EQ, LIKE, MIN };

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

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

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

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

  class Filter {

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

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

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

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

  }     

}

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

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

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

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

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

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

produkRepository.findAll(filterSpecification, pageable);

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

Melakukan filtering pada tabel

Melakukan filtering pada tabel

Belajar Membuat Aplikasi Web Dengan Spring Framework

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

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

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

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

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

Struktur package di aplikasi web

Struktur package di aplikasi web

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

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

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

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

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

Validasi secara otomatis

Validasi secara otomatis

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

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

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

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

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

}

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

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

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

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

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

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

  @Inject
  private WebFlowConfig webFlowConfig;

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

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

  ...
}

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

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

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

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

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

    return emf;
  }

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

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

}

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

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

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

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

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

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

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

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

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

Tampilan pesan kesalahan pada Ajax

Tampilan pesan kesalahan pada Ajax

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

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

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

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

Contoh flow di SWF

Contoh flow di SWF

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

<flow ... abstract="true">

    <input name="selectedEntity" />    

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

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

    <end-state id="kembali" />

</flow>

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

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

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

  <view-state id="entry">

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

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

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

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

  </view-state>       

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

</flow>

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

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

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

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

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

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

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

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

Contoh tampilan data entry

Contoh tampilan data entry

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

Contoh tampilan edit

Contoh tampilan edit

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

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

Contoh tampilan tampilan tabel

Contoh tampilan tampilan tabel

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

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

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

Untuk membuat persistence layer, saya cukup membuat interface seperti:

public interface KonsumenRepository extends JpaRepository<Konsumen, Long> {

  Page findByNamaLike(String nama, Pageable pageable);

  Page findByAlamatLike(String alamat, Pageable pageable);

  Page findByKota_NamaLike(String namaKota, Pageable pageable);

}

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

Pada service layer, saya membuat class seperti:

@Service @Transactional
public class KonsumenService {

  @Inject
  private transient KonsumenRepository konsumenRepository;

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

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

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

}

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

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

@Component
public class KonsumenAction {

  @Inject
  private KonsumenService konsumenService;

  ...

}

Saya kemudian bisa memakainya seperti pada:

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

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

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

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

Tampilan pesan informasi melalui growl

Tampilan pesan informasi melalui growl

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

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

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

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

Menyimpan Template simple-escp Di Database

Ada beberapa pengguna aplikasi yang sering kali ingin mengubah hasil percetakan di printer dari waktu ke waktu. Bila kode program percetakan disimpan di dalam aplikasi, ini berarti saya harus mengubah kode program setiap kali ada perubahan layout. Hal ini lama-lama bisa merepotkan! Oleh sebab itu, pada artikel Memakai simple-escp Di Griffon, saya mendefinisikan template percetakan dalam bentuk file JSON. Setiap kali ada yang ingin merubah layout percetakan, mereka bisa meng-edit file JSON ini sendiri tanpa harus menunggu saya.

Solusi di atas bekerja dengan baik bila template laporan terletak di server yang dipakai bersama (misalnya aplikasi web). Bagaimana dengan aplikasi desktop yang dikembangkan dengan menggunakan Griffon dan simple-jpa? Saya dapat menyimpan template JSON untuk simple-escp ke dalam database. Dengan demikian, perubahan template yang dibuat oleh satu pengguna tetap dapat dilihat dan dipakai oleh pengguna lainnya.

Sebagai contoh, saya bisa membuat sebuah entity untuk mewakili template simple-escp seperti berikut ini:

@DomainClass @Entity @Canonical
class TemplateFaktur {

    @NotBlank
    String nama

    @Lob
    String isi

}

Saya memakai annotation @Lob pada field isi untuk menandakan bahwa kolom tersebut dapat di-isi dengan banyak karaketer (large object type). Pada Hibernate JPA dan database MySQL Server, kombinasi ini akan menghasilkan field dengan tipe longtext yang dapat menampung hingga maksimum 4 GB karakter (bandingkan dengan VARCHAR yang menampung maksimal 255 karakter).

Untuk mencetak template simple-escp berdasarkan TemplateFaktur yang sudah disimpan di database, saya dapat menggunakan kode program seperti berikut ini:

TemplateFaktur template = findTemplateFakturByNama(model.nama)
JsonTemplate template = new JsonTemplate(template.isi)
PrintPreviewPane printPreviewPane = view.printPreviewPane
printPreviewPane.display(template, DataSources.from(model.dataSource, model.options))

Sekarang, saya perlu membuat sebuah MVC untuk mengedit template yang ada. Agar pengguna bisa lebih nyaman dalam meng-edit template, saya akan menggunakan groovy.ui.ConsoleTextEditor bawaan Groovy. ConsoleTextEditor mengandung sebuah JTextPane yang dilengkapi dengan syntax highlighting sehingga sangat berguna untuk menampilkan dokumen teks yang memiliki syntax seperti bahasa pemograman. Sebagai contoh, saya bisa mendefinisikan view seperti berikut ini:

actions {
    action(id: 'cari', name: 'Cari', closure: controller.cari)
    action(id: 'simpan', name: 'Simpan', closure: controller.simpan)
    action(id: 'reset', name: 'Reset', closure: controller.reset)
}

panel(id: 'mainPanel') {
    borderLayout()

    panel(constraints:PAGE_START) {
        flowLayout(alignment: FlowLayout.LEFT)
        comboBox(id: 'namaTemplateFaktur', model: model.namaTemplateFaktur)
        button(action: cari)
    }

    widget(new ConsoleTextEditor(), id: 'inputEditor', constraints: CENTER)

    panel(constraints: PAGE_END) {
        flowLayout(alignment: FlowLayout.LEFT)
        button(action: simpan)
        button(action: reset)
    }
}

Untuk menampilkan template dari database untuk di-edit, saya dapat menggunakan kode program seperti berikut ini di controller:

def cari = {
    execInsideUISync {
        String namaTemplateFaktur = model.namaTemplateFaktur.selectedItem
        if (namaTemplateFaktur) {
            String isi = findTemplateFakturBy(namaTemplateFaktur)?.isi
            TextEditor textEditor = view.inputEditor.textEditor
            DefaultStyledDocument doc = new DefaultStyledDocument()
            doc.setDocumentFilter(new SimpleEscpFilter(doc))
            doc.insertString(0, isi?: '', null)
            textEditor.setDocument(doc)
            textEditor.caretPosition = 0
        }
    }
}

Secara default, ConsoleTextEditor akan melakukan syntax highlighting berdasarkan format Groovy. Karena simple-escp memakai format yang berbeda, saya akan mendefinisikan sebuah DocumentFilter baru yang saya sebut sebagai SimpleEscpFilter yang isinya seperti berikut ini:

class SimpleEscpFilter extends StructuredSyntaxDocumentFilter {

    public static final String VARIABLES = /(?ms:${.*?})/
    public static final String FUNCTIONS = /(?ms:%{.*?})/
    public static final String CODE = /(?ms:{{.*?}})/

    SimpleEscpFilter(DefaultStyledDocument document) {
        super(document)

        StyleContext styleContext = StyleContext.getDefaultStyleContext()
        Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE)

        Style variables = styleContext.addStyle(VARIABLES, defaultStyle)
        StyleConstants.setForeground(variables, Color.GREEN.darker().darker())
        getRootNode().putStyle(VARIABLES, variables)

        Style functions = styleContext.addStyle(FUNCTIONS, defaultStyle)
        StyleConstants.setForeground(functions, Color.BLUE.darker().darker())
        getRootNode().putStyle(FUNCTIONS, functions)

        Style code = styleContext.addStyle(CODE, defaultStyle)
        StyleConstants.setForeground(code, Color.MAGENTA.darker().darker())
        getRootNode().putStyle(CODE, code)
    }

}

StructuredSyntaxDocumentFilter adalah DocumentFilter bawaan Groovy yang melakukan syntax highlighting berdasarkan ekspresi Regex. Pada implementasi di atas, saya memberikan pewarnaan yang berbeda untuk setiap komponen dalam template simple-escp. Bila saya menjalankan program, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan editor

Tampilan editor

Bila pengguna men-klik tombol simpan, saya dapat menyimpan perubahan dengan kode program seperti berikut ini:

def simpan = {
    TemplateFaktur templateFaktur = findTemplateFakturByNama(namaTemplateFaktur)
    if (!templateFaktur) {
       templateFaktur = new TemplateFaktur(nama: model.namaTemplateFaktur.selectedItem)
       persist(templateFaktur)
    }
    templateFaktur.isi = view.inputEditor.textEditor.text
}

Sekarang, saya tidak perlu khawatir lagi harus men-deploy ulang aplikasi hanya karena perubahan kecil di layout percetakan. Bila pengguna tidak memiliki tim IT yang memahami syntax simple-escp, setidaknya saya masih bisa mengirim email berisi template yang sudah dimodifikasi untuk di-copy paste oleh mereka. Ini masih jauh lebih baik daripada harus men-deploy ulang aplikasi 😀

Melakukan Query Full-Text Search Dengan Hibernate Search

Pada artikel Memakai Full-Text Search Di MySQL Server, saya menunjukkan bagaimana menerapkan Full-Text Search (FTS) dengan menggunakan MySQL Server. Kali ini, saya akan mencoba alternatif lain dimana FTS dilakukan dari sisi aplikasi dengan menggunakan Hibernate Search. Dengan demikian, FTS tetap dapat dilakukan pada database apapun yang didukung oleh Java. Tidak perlu lagi terikat pada syntax MATCH(...) AGAINST(...) yang hanya berlaku di MySQL Server.

Saya tidak akan ragu menggunakan Hibernate Search pada aplikasi web. Akan tetapi pada pada aplikasi desktop yang tidak memiliki server aplikasi, ia bisa menimbulkan kerumitan baru. Agar index yang dibuat Hibernate Search dapat diperbaharui dan dipakai bersama oleh banyak klien (disebut juga slave), maka dibutuhkan sebuah program yang menjalankan Hibernate Search sebagai master. Hibernate Search mendukung kasus seperti ini dengan memanfaatkan JMS atau JGroups. Tentu saja ini akan lebih repot bila dibandingkan dengan memakai fitur yang sudah ada di server database.

Sebagai latihan, saya akan memakai Hibernate Search pada sebuah aplikasi Griffon. Saya akan mulai dengan menambahkan dependency di BuildConfig.groovy seperti berikut ini:

compile('org.hibernate:hibernate-search-orm:5.0.0.Final')

Hibernate Search terintegrasi sangat baik dengan Hibernate JPA. Hal ini bisa terlihat dari pengaturan Hibernate Search yang dilakukan pada persistence.xml milik JPA. Sebagai contoh, saya menambahkan baris konfigurasi berikut ini:

<?xml...>
  <persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <class>domain.inventory.Produk</class>
    ...
    <properties>
      ...
      <property name="hibernate.search.default.directory_provider" value="filesystem"/>
      <property name="hibernate.search.default.indexBase" value="C:/indexes"/>
    </properties>
  </persistence-unit>
</persistence>

Hibernate Search akan memakai Apache Lucene untuk melakukan indexing. Pada konfigurasi di atas, saya akan memakai DirectoryProvider berupa FSDirectoryProvider yang akan menyimpan index Lucene ke dalam folder yang telah ditentukan oleh hibernate.search.default.indexBase.

Berikutnya, saya akan memberikan annotation Hibernate Search pada entity JPA yang sudah ada. Saya perlu menambahkan @Indexed bila ingin entity JPA diproses. Selain itu, saya perlu menambahkan @Field pada property yang hendak di-index. Sebagai contoh, entity JPA saya akan terlihat seperti berikut ini:

@DomainClass @Entity
@Indexed
class Produk {

    ...

    @NotBlank @Size(min=3, max=150)
    @Field
    String nama

    ...

}

Sekarang, setiap kali entity JPA ditambah, dimodifikasi atau di-update, maka index akan diperbaharui. Saya akan menemukan informasi index disimpan di folder yang saya tentukan pada hibernate.search.default.indexBase. Isi file dalam index sesuai dengan format yang dipakai oleh Apache Lucene.

Lalu, bagaimana bila saya memiliki data lama yang belum ada index-nya? Saya dapat menghasilkan index untuk data yang sudah ada dengan menggunakan kode program seperti berikut ini:

Search.getFullTextEntityManager(getEntityManager()).createIndexer().startAndWait()

Untuk melakukan query, saya bisa memakai cara yang JPA-friendly seperti pada contoh berikut ini:

import simplejpa.SimpleJpaUtil
import org.hibernate.search.jpa.FullTextEntityManager
import org.hibernate.search.jpa.Search
import org.hibernate.search.query.dsl.QueryBuilder
import javax.persistence.Query
import domain.Produk

def produkRepository = SimpleJpaUtil.instance.repositoryManager.findRepository('Produk')
produkRepository.withTransaction {
   FullTextEntityManager fem = Search.getFullTextEntityManager(entityManager)
   QueryBuilder b = fem.getSearchFactory().buildQueryBuilder().forEntity(Produk).get()
   def luceneQuery = b.keyword().onField("nama").matching("digital").createQuery();
   def hasil = fem.createFullTextQuery(luceneQuery).getResultList()   
   println hasil
}

Kode program di atas akan mencetak semua produk yang namanya mengandung kata 'digital'. Method fem.createFullTextQuery() akan menghasilkan sebuah Query JPA biasa yang memiliki method getResultList() untuk menghasilkan List berisi object Produk yang ditemukan.

org.hibernate.search.query.dsl.QueryBuilder adalah builder bawaan Hibernate Search untuk mempermudah programmer sehingga tidak perlu membuat query Lucene secara langsung. Builder tersebut menyediakan fluent API untuk menghasilkan sebuah org.apache.lucene.search.Query pada saat createQuery() dipanggil.

Untuk menunjukkan kemampuan FTS pada Hibernate Search, saya bisa menambahkan fuzzy() pada QueryBuilder seperti berikut ini:

def luceneQuery = b
  .keyword()
  .fuzzy()
  .onField("nama")
  .matching("telephone")
  .createQuery();

Query di atas akan mencari nama produk yang mengandung nama 'telephone' dan yang mendekatinya! Pada produk dengan nama Indonesia, kata yang paling sering digunakan adalah 'telepon'. Bila terdapat .fuzzy(), maka produk yang namanya mengandung 'telepon' juga akan ikut disertakan pada hasil pencarian.

Bila saya melakukan pencarian untuk lebih dari 1 kata seperti pada:

def luceneQuery = b
  .keyword()
  .onField("nama")
  .matching("kabel telepon")
  .createQuery();

maka object Produk yang memiliki nama 'kabel telepon' akan ditampilkan pada baris paling awal. Produk lain yang hanya mengandung kata 'kabel' atau 'telepon' tetap akan dikembalikan, tetapi pada urutan yang lebih rendah.

Walaupun membantu dalam melakukan FTS, saya menemukan kendala dimana versi Hibernate Search yang saya pakai tidak mendukung named entity graph yang diperkenalkan oleh JPA 2.1. Tanpa named entity graph, saya harus menentukan apa saja relasi yang perlu di-fetch secara manual setiap kali melakukan pencarian.

Mengatasi Permasalahan N+1 Pada Query Di Hibernate

Apa itu permasalahan N+1? Sebagai contoh, anggap saja saya memiliki sebuah JPA entity seperti berikut ini:

@NamedEntityGraphs([
    @NamedEntityGraph(name='FakturJualOlehSales.Piutang', attributeNodes=[
        @NamedAttributeNode('listItemFaktur'),
        @NamedAttributeNode('piutang')
    ])
])
class FakturJualOlehSales extends FakturJual {

    @NotNull(groups=[Default,InputPenjualanOlehSales]) @ManyToOne
    Konsumen konsumen

    @NotNull(groups=[Default]) @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate jatuhTempo

    @OneToOne(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY)
    KewajibanPembayaran piutang

    @OneToOne(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY)
    BonusPenjualan bonusPenjualan

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    @JoinTable(name='FakturJual_retur')
    @OrderColumn
    List<PenerimaanBarang> retur = []

    ...

}

Class FakturJualOlehSales memiliki banyak relasi dengan class lainnya. Ini adalah sesuatu yang wajar terutama bila perancangan dilakukan dengan menggunakan metode DDD yang mengedepankan aggregation. Sebagai turunan dari FakturJual, class FakturJualOlehSales memiliki relasi one-to-many dengan ItemFaktur. Class FakturJualOlehSales juga memiliki relasi one-to-one dengan KewajibanPembayaran (untuk mewakili piutang yang ditimbulkan oleh faktur ini) dan BonusPenjualan (untuk mewakili bonus yang diberikan pada faktur ini). Class KewajibanPembayaran selanjutnya memiliki relasi one-to-many dengan Pembayaran. Begitu juga, class BonusPenjualan selanjutnya memiliki relasi one-to-many dengan ItemBarang. Selain itu, class FakturJualOlehSales juga memiliki relasi one-to-many dengan PenerimaanBarang untuk mewakili retur.

Sebuah class dengan relasi yang kompleks, bukan? Tapi saya tidak selalu butuh semua nilai relasi yang ada setiap kali berhadapan dengan FakturJualOlehSales. Sebagai contoh, pada screen untuk entry data faktur, saya tidak perlu menampilkan nilai piutang. Tetapi pada screen untuk memasukkan data pembayaran piutang, saya perlu nilai piutang tetapi tidak perlu informasi seperti bonusPenjualan.

Oleh sebab itu, saya memberikan nilai atribut fetch=FetchType.LAZY pada beberapa atribut agar Hibernate tidak men-query atribut tersebut. Teknik ini disebut lazy loading. Nilai dari atribut piutang atau bonusPenjualan hanya akan di-query pada saat ia diakses. Ini hanya berlaku selama entity masih berada dalam pengelolaan EntityManager. Bila sudah diluar cakupan EntityManager, saya akan memperoleh pesan kesalahan LazyLoadingException yang sangat terkenal.

Cara lain untuk membaca nilai yang lazy adalah dengan menggunakan query JP QL yang melakukan join fetch secara manual. Khusus untuk JPA 2.1, pilihan yang lebih nyaman adalah dengan menggunakan fasilitas named entity graph. Dengan fasilitas ini, saya tidak perlu menghabiskan banyak waktu memikirkan query! Sebagai contoh, saya mendeklarasikan sebuah named entity graph dengan nama FakturJualOlehSales.Piutang yang akan menyertakan nilai atribut piutang pada saat FakturJualOlehSales dibaca dari database.

Berikut adalah contoh kode program yang memakai named entity graph melalui simple-jpa:

FakturJualRepository repo = simplejpa.SimpleJpaUtil.instance
    .repositoryManager.findRepository('FakturJual')

int start = System.currentTimeMillis();
repo.findAllFakturJualOlehSalesFetchPiutang();
int stop = System.currentTimeMillis();

println "Delta = ${stop-start}"

Walaupun kode program di atas terlihat sederhana, tapi kinerjanya tidak memuaskan! Hibernate ternyata mengerjakan sangat banyak query, seperti:

select distinct ... from FakturJual fakturjual0_ left outer join KewajibanPembayaran kewajibanp1_ on fakturjual0_.piutang_id=kewajibanp1_.id left outer join FakturJual_listItemFaktur listitemfa2_ on fakturjual0_.id=listitemfa2_.FakturJual_id where ...

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

...

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

...

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

...

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

Permasalahan ini sering kali disebut sebagai permasalahan N+1. Nilai 1 adalah query pertama untuk SELECT * FROM x. Setelah itu, untuk N jumlah record yang diperoleh dari query pertama, lakukan query lain untuk membaca nilai di tabel lain seperti SELECT * FROM y WHERE y.x_id = x.id. Dengan demikian, semakin banyak jumlah record yang hendak dibaca, semakin banyak juga query tambahan yang perlu dilakukan. Permasalahan N+1 biasanya adalah pemborosan kinerja yang tak seharusnya terjadi karena ia dapat digantikan dengan join dan/atau subquery.

Sebagai patokan, saya akan menyimpan hasil eksekusi program di atas dan membuat versi grafik-nya yang terlihat seperti pada gambar berikut ini:

Grafik yang menunjukkan kinerja awal

Grafik yang menunjukkan kinerja awal

Pada grafik di atas, pada eksekusi pertama kali, saya akan memperoleh penalti kinerja yang cukup tinggi. Ini adalah karakteristik dari simple-jpa. Selain itu, hal ini juga ditambah lagi dengan server database yang belum memiliki cache hasil query.

Saya akan mulai dengan berusaha menghilangkan query N+1 ke tabel konsumen. FakturJualOlehSales memiliki hubungan @ManyToOne dengan konsumen. Saya menemukan bahwa dengan menyertakan definisi atribut konsumen pada named entity graph, maka query N+1 untuk relasi ke konsumen tidak akan muncul lagi. Perubahan yang saya lakukan menyebabkan definisi named entity graph saya menjadi seperti berikut ini:

@NamedEntityGraph(name='FakturJualOlehSales.Piutang', attributeNodes=[
    @NamedAttributeNode('listItemFaktur'),
    @NamedAttributeNode(value='konsumen', subgraph='konsumen'),
    @NamedAttributeNode('piutang')
], subgraphs = [
    @NamedSubgraph(name='konsumen', attributeNodes=[
        @NamedAttributeNode('region'),
        @NamedAttributeNode('sales')
    ])
])

Sekarang, nilai untuk konsumen tidak akan di-query satu per satu lagi, melainkan diperoleh melalui join pada saat mengambil nilai FakturJualOlehSales seperti yang terlihat pada SQL yang dihasilkan oleh Hiberate:

select distinct ... from FakturJual fakturjual0_ ... left outer join Konsumen konsumen2_ on fakturjual0_.konsumen_id=konsumen2_.id left outer join Region region3_ on konsumen2_.region_id=region3_.id left outer join Sales sales4_ on konsumen2_.sales_id=sales4_.id ...

Sayang sekali saya tidak dapat melakukan hal yang sama untuk Produk karena listItemFaktur adalah sebuah @ElementCollection yang tidak dianggap sebuah entity sehingga tidak dapat diatur melalui named entity graph.

Sampai disini, apakah versi yang memakai left outer join akan lebih cepat dari versi N+1? Saya akan kembali melakukan sedikit percobaan dan menemukan hasil seperti yang terlihat pada gambar berikut ini:

Grafik yang menunjukkan kinerja setelah perubahan.

Grafik yang menunjukkan kinerja setelah perubahan.

Pada grafik di atas, terlihat bahwa perubahan yang saya lakukan memberikan sedikit peningkatan kinerja (sekitar 8%). Hal ini karena pada rancangan saya, tidak banyak yang bisa dioptimalkan dengan cara seperti ini.

Sebagai langkah berikutnya, saya akan menghindari query N+1 untuk retur dengan menjadikannya sebagai 1 query tunggal yang terpisah. Saya dapat menggunakan @Fetch(FetchMode.SUBSELECT) untuk keperluan seperti ini. Sebagai informasi, @Fetch adalah annotation khusus dari Hibernate dan bukan merupakan bagian dari JPA! Sebagai contoh, saya mengubah kode program menjadi seperti berikut ini:

class FakturJualOlehSales extends FakturJual {

   ...

   @OneToMany(...) @JoinTable()
   @Fetch(FetchMode.SUBSELECT)
   List<PenerimaanBarang> retur = []

   ...

}

Konfigurasi di atas akan menyebabkan seluruh query N+1 yang tadinya seperti:

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

...

digantikan oleh sebuah query tunggal yang isinya seperti berikut ini:

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id in (select fakturjual0_.id from FakturJual fakturjual0_ ...)

Saya segera menambahkan @Fetch(FetchMode.SUBSELECT) pada beberapa atribut lainnya yang memiliki permasalahan N+1. Setelah itu, saya mencoba menjalankan program dan kini memperoleh kinerja seperti yang terlihat pada grafis berikut ini:

Grafik yang menunjukkan kinerja setelah perubahan.

Grafik yang menunjukkan kinerja setelah perubahan.

Kali ini saya memperoleh peningkatan kinerja yang cukup drastis karena saya menemukan banyak atribut yang bisa dioptimalkan melalui @Fetch(FetchMode.SUBSELECT). Sebagai hasil akhir, setelah berupaya menghilangkan sebagian besar query N+1, saya memperoleh peningkatan kinerja sebesar 56%. Tidak ada perubahan yang perlu saya lakukan pada kode program yang membaca entitas; ia tetap merupakan sebuah baris yang polos seperti findAllFakturJualFetchPiutang().

Memakai c3p0 Di Hibernate JPA

Sebelum sebuah aplikasi dapat memberikan query SQL, ia harus melakukan koneksi ke server database terlebih dahulu. Setelah query SQL selesai diberikan, aplikasi biasanya menutup koneksi database. Bila hal ini (membuat dan menutup koneksi) dilakukan terus menerus setiap kali melakukan query SQL, maka akan berdampak pada kinerja aplikasi. Hal ini akan semakin terasa bila lokasi client dan server di komputer yang berbeda yang diakses melalui jaringan.

Solusi untuk masalah seperti ini adalah dengan memakai JDBC connection pool. Cara paling naif adalah hanya membuat sebuah koneksi database dan memakai ulang koneksi ini sampai aplikasi ditutup. Akan tetapi solusi ini bisa menimbulkan masalah baru, misalnya bila terjadi gangguan jaringan pada koneksi database, maka koneksi menjadi tidak valid. Koneksi yang tidak valid tersebut tidak dapat lagi dipakai untuk mengerjakan SQL. Hal ini membuat aplikasi harus ditutup agar koneksi baru kembali dibuat.

Oleh sebab itu, daripada membuat sendiri, saya bisa memakai JDBC connection pool open-source yang sudah teruji seperti c3p0 dan Apache DBCP. Connection pool yang baik akan berusaha sebisa mungkin memakai koneksi yang sudah ada dan membuat koneksi hanya baru bila dibutuhkan. Pada tulisan ini, saya akan menggunakan c3p0 pada aplikasi yang memakai Hibernate JPA. Saya dapat menemukan artifak JAR untuk c3p0 di http://mvnrepository.com/artifact/com.mchange/c3p0. Karena saya memakai Griffon, saya bisa menambahkan baris berikut ini pada BuildConfig.groovy:

griffon.project.dependency.resolution = {
    ...
    repositories {
        griffonHome()
        mavenLocal()
        mavenCentral()
    }
    dependencies {
        runtime 'com.mchange:c3p0:0.9.2.1'
        runtime 'org.hibernate:hibernate-c3p0:4.3.6.Final'
        ...
    }
}

Dependency ke com.mchange:c3p0 akan menambah JAR milik c3p0. Sementara itu, org.hibernate:hibernate-c3p0 dibutuhkan untuk memakai c3p0 pada Hibernate JPA.

Langkah terakhir untuk mengaktifkan c3p0 adalah menambah konfigurasi seperti berikut ini pada persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence ...>
  <persistence-unit ...>
    ...
    <properties>
      ...
      <property name="hibernate.connection.provider_class" value="org.hibernate.c3p0.internal.C3P0ConnectionProvider" />
      ...
    </properties>
  </persistence-unit>
</persistence>

Setelah konfigurasi di atas diberikan, Hibernate akan memakai c3p0 connection pool dengan nilai konfigurasi default. Tidak ada kode program yang perlu diberikan untuk memakai c3p0, tapi ada banyak konfigurasi yang bisa diatur. Informasi lebih lanjut mengenai konfigurasi c3p0 dapat dibaca di http://www.mchange.com/projects/c3p0/. Saya dapat memberikan nilai konfigurasi yang ada langsung pada persistence.xml atau membuat sebuah file baru dengan nama c3p0.properties. Sebagai latihan, saya akan membuat file c3p0.properties di folder resources atau root dari folder source yang isinya seperti berikut ini:

c3p0.minPoolSize = 3
c3p0.maxPoolSize = 5
c3p0.initialPoolSize = 3
c3p0.acquireRetryAttempts = 10
c3p0.testConnectionOnCheckout = true

Pada konfigurasi di atas, pada saat aplikasi dijalankan, akan ada 3 koneksi database yang dibuat (nilai dari c3p0.initialPoolSize). Ketiga koneksi ini akan dipakai ulang sebisa mungkin. Koneksi yang dibuat ini tidak harus aktif dan dipakai. Umumnya database akan mempertahankan koneksi yang tidak ditutup oleh aplikasi (baik disengaja atau tidak) atau yang berada dalam keadaan tidur. Sebagai contoh, pada MySQL Server, saya bisa memberikan perintah SQL seperti:

SHOW VARIABLES LIKE 'wait_timeout';

untuk melihat seberapa lama server MySQL akan menunggu aktifitas jaringan sebelum menutup sebuah koneksi dari client di komputer berbeda. Secara default, nilai ini adalah 28800 detik atau 8 jam.

Bila terjadi gangguan jaringan atau masalah lain yang menyebabkan aplikasi tidak dapat terhubung ke server database, maka tanpa connection pool, aplikasi akan mendapatkan kesalahan (exception). Akan tetapi, bila memakai c3p0, aplikasi akan berusaha mencoba mendapatkan koneksi baru ke server database selama berkali-kali. Saya dapat mengatur berapa kali upaya mencoba mendapatkan koneksi dengan mengisi nilai c3p0.acquireRetryAttempts. Sebagai contoh, pada konfigurasi saya, hanya setelah 10 kali upaya mendapatkan koneksi gagal baru aplikasi memperoleh kesalahan JDBC (exception).

Sebuah koneksi dalam pool bisa saja menjadi rusak atau stale. Sebagai contoh, bila server database di-restart, maka seluruh koneksi sebenarnya telah ditutup. Akan tetapi, pada sisi client, koneksi masih tertampung di pool. Seluruh koneksi yang ada di pool kini sudah tidak valid lagi dan c3p0 harusnya membuat koneksi baru. Saya bisa mengatur agar c3p0 mendeteksi koneksi yang tidak valid pada saat koneksi tersebut akan dipakai. Caranya adalah dengan memberi nilai true pada c3p0.testConnectionOnCheckout. Alternatif lainnya adalah membuat c3p0 secara periodik memeriksa koneksi yang tidak valid di pool dengan memberi nilai true pada c3p0.testConnectionOnCheckin dan nilai periode pemeriksaan dalam detik pada c3p0.idleConnectionTestPeriod.

JDBC 4 mendukung isValid() pada Connection untuk menentukan apakah koneksi tersebut masih valid atau tidak. Bila driver JDBC yang dipakai mendukung JDBC 4 (misalnya, MySQL Connector/J di versi 5.1 ke atas), maka c3p0 akan memakai isValid(). Cara ini adalah yang paling cepat. Pada driver JDBC lama, sebuah SQL dapat diberikan sebagai nilai untuk c3p0.preferredTestQuery. SQL ini akan dipakai untuk menguji apakah koneksi masih aktif atau tidak. Bila query SQL tersebut gagal, maka koneksi dianggap tidak aktif.

Fasilitas menarik lainnya dari c3p0 adalah ia mendukung JMX sehingga saya bisa menggunakan JConsole untuk melihat dan melakukan perubahan konfigurasi pada sebuah aplikasi yang sedang dijalankan tanpa harus menutup dan men-compile ulang kode program untuk aplikasi tersebut. Program JConsole dapat dijumpai di lokasi %JDK_HOME%/bin/jconsole.exe. Setelah dijalankan, saya memilih virtual machine untuk aplikasi. Setelah itu, saya dapat melihat statistik dan melakukan perubahan konfigurasi c3p0 seperti yang terlihat pada gambar berikut ini:

Melakukan Administrasi c3p0 Melalui JMX

Melakukan Administrasi c3p0 Melalui JMX

Beberapa operasi membutuhkan username dan password yang isinya harus sesuai dengan yang diberikan pada saat membuat koneksi JDBC.

Memakai simple-escp Pada Aplikasi Web

Pengguna bisa mencetak melalui browser dengan memilih menu File, Print.. di browser atau melalui JavaScript window.print(). Percetakan yang dilakukan dengan cara seperti ini adalah percetakan graphic mode. Bagaimana bila yang diinginkan adalah percetakan text mode? Pada percetakan text mode, posisi bisa ditentukan secara lebih akurat dan pengaturan halaman dapat dilakukan dalam satuan baris dan karakter. simple-escp adalah salah satu library Java yang dapat dipakai untuk keperluan ini. Untuk memakai simple-escp pada aplikasi web, saya dapat menyertakannya sebagai applet. Applet adalah kode program Java yang akan dikerjakan di sisi client di browser (sama seperti Adobe Flash, Microsoft Silverlight, ActiveX, dan sebagainya). Salah satu fitur andalan applet adalah kode program JavaScript di halaman HTML yang sama dapat dipakai untuk memanipulasi applet. Begitu juga sebaliknya, applet juga dapat memanggil kode program JavaScript yang dideklarasikan pada halaman HTML yang sama.

Untuk membuat applet, saya dapat menggunakan framework Griffon. Saya akan mulai dengan membuat proyek baru dengan memberikan perintah:

griffon create-app simple-escp-applet

Karena applet ini akan memanggil simple-escp, saya perlu mengubah bagian griffon.project.dependency.resolution dari griffon-app/conf/BuildConfig.groovy menjadi seperti berikut ini:

griffon.project.dependency.resolution = {
    // inherit Griffon' default dependencies
    inherits("global") {
    }
    log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
    repositories {
        griffonHome()
        mavenRepo "http://dl.bintray.com/jockihendry/maven"
    }
    dependencies {
        compile 'jockihendry:simple-escp:0.4'
    }
}

Saya juga perlu mengubah bagian webstart agar nilai codebase merujuk ke lokasi URL di server nanti, seperti pada contoh berikut ini:

production {
    ...
    griffon {
       ...
       webstart {
          codebase = "http://localhost/cetak"
       }
    }
}

Selain itu, saya akan memanggil JavaScript yang ada di HTML melalui Applet melalui netscape.javascript.JSObject. Class ini terletak pada plugin.jar dan tidak tersedia pada saat kompilasi. Untuk itu, saya men-copy file tersebut dari C:\Program Files\Java\jdk\jre\lib ke folder lib di proyek Griffon.

Karena saya perlu melakukan signing pada applet yang dihasilkan Griffon, maka saya mengubah bagian signingkey di environment production menjadi seperti berikut ini:

production {
   signingkey {
      params {                
         storepass = 'thesolidsnake'
         keypass = 'thesolidsnake'
         lazy = false // sign, regardless of existing signatures
      }
   }
   ...
}

Sekarang, saya siap untuk membuat kode program. Satu hal yang menjadi kendala adalah DataSource. Saat ini simple-escp menerima DataSource dalam bentuk Map atau JavaBean object yang merupakan tipe data Java. Kedua data source tersebut tidak dapat dipakai di JavaScript. Untuk itu, saya perlu membuat DataSource baru yang bisa membaca data dalam bentuk object JavaScript di halaman HTML yang sama. simple-escp memungkinkan pengguna membuat implementasi DataSource sendiri seperti yang terlihat pada kode program berikut ini (saya meletakkannya di src\main\datasource\JSONDataSource.groovy):

package datasource;

import simple.escp.data.DataSource;
import simple.escp.exception.InvalidPlaceholder;
import javax.json.Json;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonValue;
import java.io.StringReader;

public class JSONDataSource implements DataSource {

    private String jsonString;
    private JsonObject json;

    public JSONDataSource(String jsonString) {
        this.jsonString = jsonString;
        try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
            json = reader.readObject();
        }
    }

    @Override
    public boolean has(String s) {
        return json.containsKey(s);
    }

    @Override
    public Object get(String member) throws InvalidPlaceholder {
        JsonValue value = json.get(member);
        if (value.getValueType() == JsonValue.ValueType.ARRAY) {
            return value;
        } else if (value.getValueType() == JsonValue.ValueType.NUMBER) {
            return ((JsonNumber) value).bigDecimalValue();
        } else {
            return value.toString();
        }
    }

    @Override
    public Object getSource() {
        return jsonString;
    }

    @Override
    public String[] getMembers() {
        return json.keySet().toArray(new String[0]);
    }
}

Saya kemudian mengubah isi dari SimpleEscpAppletView.groovy menjadi seperti berikut ini:

package simple.escp.applet

import java.awt.BorderLayout

application() {

    panel(id: 'mainPanel', layout: new BorderLayout()) {

    }

}

Sebagai langkah terakhir dalam membuat applet, saya mengubah kode program SimpleEscpAppletController.groovy menjadi seperti berikut ini:

package simple.escp.applet

import datasource.JSONDataSource
import simple.escp.fill.FillJob
import simple.escp.json.JsonTemplate
import simple.escp.swing.PrintPreviewPane
import sun.plugin.javascript.JSObject
import javax.swing.JPanel
import java.awt.BorderLayout

class SimpleEscpAppletController {

    def model
    def view

    void mvcGroupInit(Map args) {
        def window = JSObject.getWindow(app)
        def report = new JsonTemplate(window.eval("JSON.stringify(template);")).parse()
        def dataSource = window.eval("JSON.stringify(source);");
        def result = new FillJob(report, new JSONDataSource(dataSource)).fill()
        PrintPreviewPane pane = new PrintPreviewPane(result,
            report.pageFormat.pageLength, report.pageFormat.pageWidth)
        JPanel mainPanel = view.mainPanel
        mainPanel.add(pane, BorderLayout.CENTER)
    }

}

Untuk menghasilkan applet, saya memberikan perintah berikut ini:

griffon package applet

Griffon secara otomatis akan menghasilkan file distribusi applet pada folder dist/applet. Pada folder ini, selain file JAR, saya juga akan menemukan file applet.jnlp yang dapat dipakai untuk memanggil applet di HTML. Seluruh file JAR juga sudah di-sign dengan key yang di-generate untuk keperluan sementara. Saya juga dapat menemukan file applet.html yang berisi contoh pemanggilan applet.

Berikutnya, saya memindahkan seluruh file JAR (dan juga versi yang sudah di-compress dalam bentuk .jar.pack.gz), file gambar, file JNLP dan file HTML ke lokasi deployment di webserver. Sebagai contoh, karena saya memakai NGINX, saya akan memindahkan file tersebut ke folder html/cetak. Saya kemudian mengubah file applet.html menjadi seperti berikut ini:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <title>Latihan Cetak</title>
    <script>
    var template = {
        pageFormat: {
            pageLength: 10,
            pageWidth: 50,
            usePageLengthFromPrinter: false
        },
        template: {
            header: [" PT. XYZ                                    HAL: %{PAGE_NO}"],
            detail: [
                " Nomor Faktur: ${nomorFaktur:20}",
                {
                    "table": "listItemFaktur",
                    "border": true,
                    "columns": [
                        { caption: "Nama Barang", source: "namaBarang", width: 25, wrap: true },
                        { caption: "Qty", source: "qty::right", width: 5 },
                        { caption: "Harga", source: "harga.bigDecimalValue()::right::number", width: 15 }
                    ]
                }
            ]
        }
    };

    var source = {
        nomorFaktur: 'FA-1234-556677-XX-BB-CC',
        listItemFaktur: [
            {namaBarang: 'Plantronics Backbet Go 2 With Charging Case', qty: 1, harga: 13750000},
            {namaBarang: 'CORT Gitar Akustik AD810 - Natural Satin', qty: 1, harga: 14900000},
            {namaBarang: 'SAMSON Monitor Speaker System MediaOne 3A', qty: 1, harga: 14250000}
        ]
    };

  </script>
</head>
<body>

<h1>Latihan Cetak</h1>

<p>Halaman ini akan memanggil applet yang mencetak ke printer dot matrix dengan bantuan JavaScript.</p>

<APPLET CODEBASE='http://localhost/cetak'
        CODE='griffon.swing.SwingApplet'
        ARCHIVE='griffon-swing-runtime-1.4.0.jar,griffon-rt-1.5.0.jar,groovy-all-2.2.1.jar,javax.json-1.0.4.jar,javax.json-api-1.0.jar,jcl-over-slf4j-1.7.5.jar,jul-to-slf4j-1.7.5.jar,log4j-1.2.17.jar,plugin.jar,simple-escp-0.4.jar,simple-escp-applet-0.1.jar,slf4j-api-1.7.5.jar,slf4j-log4j12-1.7.5.jar'
        WIDTH='800' HEIGHT='500'>
    <PARAM NAME="java_arguments" VALUE="-Djnlp.packEnabled=true">
    <PARAM NAME='jnlp_href' VALUE='http://localhost/cetak/applet.jnlp'>
    <PARAM NAME='dragggable' VALUE='true'>
    <PARAM NAME='image' VALUE='griffon.png'>
    <PARAM NAME='boxmessage' VALUE='Loading Simple-escp-applet'>
    <PARAM NAME='boxbgcolor' VALUE='#FFFFFF'>
    <PARAM NAME='boxfgcolor' VALUE='#000000'>
    <PARAM NAME='codebase_lookup' VALUE='false'>
</APPLET>

</body>
</html>

Pada HTML di atas, saya mendeklarasikan dua variabel JavaScript yang wajib ada, yaitu variabel template dan source. Kedua variabel ini akan dibaca oleh applet dan masing-masing diterjemahkan menjadi JsonTemplate dan JSONDataSource. Pada kasus yang lebih kompleks, ini variabel source biasanya akan dihasilkan oleh sisi server. Sebagai contoh, bila bahasa pemograman di sisi server adalah PHP, saya dapat menggunakan function json_encode() untuk menghasilkan object JavaScript dari array atau object PHP yang mengimplementasikan JsonSerializable.

Sekarang, saya akan mencoba membuka halaman http://localhost/cetak/applet.html melalui browser pada sistem operasi yang telah memiliki instalasi Java. Saya menemukan dialog peringatan seperti berikut ini:

Peringatan di browser pada saat menjalankan applet

Peringatan di browser pada saat menjalankan applet

Karena saya memang ingin mengaktifkan Java di browser, maka saya memilih Later. Setelah itu, akan kembali muncul pesan peringatan lainnya seperti pada gambar berikut ini:

Peringatan saat menjalankan applet yang tidak sertifikatnya tidak diverifikasi

Peringatan saat menjalankan applet yang tidak sertifikatnya tidak diverifikasi

Pesan peringatan ini muncul karena JAR saya di-sign sendiri tanpa verifikasi dari pihak terpercaya seperti Verisign, GoDaddy dan sejenisnya (yang memungut biaya verifikasi). Pesan keamanan ini diberikan karena applet yang di-sign oleh publisher tak dikenal memiliki akses yang lebih leluasa dibandingkan applet yang tidak di-sign sama sekali, misalnya baca/tulis file serta melakukan percetakan.

Saya memberi tanda centang pada I accept the risk and want to run this application. dan men-klik tombol Run. Tampilan browser saya akan terlihat seperti pada gambar berikut ini:

Tampilan di browser

Tampilan di browser

Bila saya men-klik tombol Print dari PrintPreviewPane milik simple-escp, maka percetakan akan dilakukan pada text mode ke printer dot matrix yang sedang terpilih.

Mencetak Ke Printer Dot-Matrix Di Java Dengan simple-escp

Salah satu teknologi lama yang masih bertahan sampai saat ini adalah printer dot matrix. Salah satu alasan utama
penggunaan printer tersebut adalah untuk mencetak pada kertas karbon yang secara otomatis membuat duplikasi ke beberapa halaman berbeda. Berdasarkan data dari Wikipedia, hingga tahun 2013, hanya ada 1 perusahaan di Canada, 1 perusahaan di United Kingdom, dan 2 perusahaan di Amerika yang masih memproduksi kertas karbon. Di India (dan juga Indonesia), penggunaan kertas karbon untuk mengisi formulir masih sering dijumpai. Teknologi kertas karbon sebenarnya sudah kalah bersaing menghadapi mesin fotokopi (photocopying), printer laser yang semakin murah dan penggunaan formulir elektronik.

Printer dot matrix mencetak karakter berdasarkan font internal (dalam RAM printer) dan bekerja dengan sangat baik di DOS. Hal ini karana karakter ganti baris di DOS yang berupa 2 karakter, CR + LF, sehingga file DOS dapat langsung dicetak ke printer (saat itu belum ada driver terpisah untuk masing-masing printer). Tradisi ini juga dibawa ke sistem operasi Windows (bandingkan dengan Linux dan Unix yang hanya memakai karakter LF untuk ganti baris).

Seiring dengan perkembangan printer inkjet yang dapat mencetak piksel secara fleksibel dan lebih sunyi, sistem operasi modern mulai beralih mendukung mereka. Printer inkjet memiliki bahasa percetakan yang lebih mudah distandarisasikan. Sebagai contoh, sistem operasi bisa memakai font di sisi software yang berlaku untuk semua printer (di Windows, ini bisa dijumpai di folder C:\Windows\Fonts). Dukungan software pun beralih meninggalkan dot matrix dengan asumsi bahwa teknologi dot matrix akan mati dengan sendirinya.

Sampai sekarang, masih ada produsen yang tetap memproduksi printer dot matrix dan memasarkannya dengan harga yang mahal (akibat monopoli). Lalu apakah printer dot matrix tersebut dapat dipakai pada sistem operasi modern? Yup! Bisa! Mereka dilengkapi dengan driver yang akan menerjemahkan gambar dan dokumen untuk dicetak pada modus grafis oleh printer sehingga dapat bekerja pada software modern seperti Microsoft Office. Percetakan graphic mode bukanlah percetakan text mode (atau raw mode) yang mencetak karakter per karakter. Percetakan graphic mode lebih lambat dan pengaturan posisi karakter lebih sulit dilakukan dibandingkan pada text mode.

Salah satu solusi untuk mendapatkan posisi percetakan yang rapi di graphic mode adalah memakai font bawaan printer. Microsoft Word, misalnya, akan menampilkan font bawaan printer bila printer dot matrix dipilih sebagai default printer. Mengubah printer dot matrix tersebut menjadi default printer adalah sesuatu yang wajib karena font disimpan di dalam RAM printer (disebut sebagai printer device font), bukan di C:\Windows\Fonts.

Lalu, apakah saya dapat menggunakan printer device font di iReport atau JasperReport? Sayang sekali, jawabannya tidak! Java dirancang untuk tidak terikat pada satu platform tertentu. Memakai printer device font bukan hanya terikat pada sistem operasi, tapi juga terikat pada ketersediaan sebuah hardware printer merk tertentu. Pada komputer yang tidak memiliki printer ini, tampilan bisa jadi berantakan. Dengan demikian, saya tidak dapat memakai JasperReport untuk menghasilkan laporan text mode yang memiliki posisi dan ukuran yang bisa saya kendalikan secara mudah.

Walaupun demikian, saya masih dapat memakai Java Print Service (JPS) untuk mencetak text mode di Java. Hanya saja, tanpa kemampuan templating seperti di JasperReport, ini akan menjadi sebuah tantangan tersendiri. Untuk mengatasi masalah ini, saya akan menggunakan simple-escp yang menyediakan fasilitas templating dan preview khusus untuk text mode. simple-escp juga menyediakan cara mudah untuk memberikan perintah ESC/P (Epson Standard Code for Printers) untuk melakukan pengaturan pada printer. Dokumentasi untuk simple-escp dapat dibaca di http://jockihendry.github.io/simple-escp/. Pada artikel ini, saya akan mencoba membuat sebuah program Java sederhana yang mencetak ke printer dot-matrix dengan menggunakan simple-escp.

Langkah pertama yang saya lakukan adalah men-download file JAR simple-escp di lokasi https://github.com/JockiHendry/simple-escp/releases. Berikutnya, saya akan membuat proyek baru di IntelliJ IDEA dengan memilih menu File, New Project. Pada dialog yang muncul, saya memilih Java dan men-klik tombol Next dua kali, mengisi nama proyek dan men-klik tombol Finish.

Saya kemudian men-klik kanan nama proyek dan memilih menu Open Module Settings. Pada dialog yang muncul, saya men-klik tombol tambah, lalu memilih Jar or Directories… untuk menambahkan JAR milik simple-escp sehingga dialog terlihat seperti pada gambar berikut ini:

Menambah library simple-escp

Menambah library simple-escp

Saya kemudian men-klik tombol Ok dua kali.

Berikutnya, saya akan membuat sebuah kode program yang akan melakukan percetakan. Untuk itu, saya men-klik kanan folder src dan memilih menu New, Java Class. Saya memberi nama Main pada class tersebut dan mengisinya seperti pada kode program berikut ini:

import simple.escp.SimpleEscp;
import simple.escp.util.EscpUtil;

public class Main {

    public static void main(String[] args) {
        SimpleEscp simpleEscp = new SimpleEscp("EPSON LX-310 ESC/P");
        simpleEscp.print(EscpUtil.escSelectUnderline() +
            "Ini akan digarisbawahi" + EscpUtil.escCancelUnderline());
    }

}

Setelah itu, saya men-klik bagian yang kosong pada kode program class tersebut dan memilih menu Run Main.main().

Begitu program dijalankan, ia akan langsung mencetak ke printer yang bernama EPSON LX-310 ESC/P. Tulisan yang dicetak akan memiliki garis bawah. Untuk melihat daftar nama printer di Windows, klik Start Menu di pojok kiri bawah layar dan pilih Devices and Printers.

Biasanya akan ada 1 printer yang dianggap sebagai default printer yang menjadi target percetakan. Bila seandainya printer dot matrix yang ingin saya pakai sudah di-set sebagai default printer di sistem operasi, maka saya dapat mengganti perintah new SimpleEscp("NAMA_PRINTER") menjadi new SimpleEscp(). Membuat instance SimpleEscp tanpa parameter akan secara otomatis memakai default printer.

Rasanya tidak cukup bila hanya bisa mencetak dan memberikan perintah ESC/P saja. Kebutuhan percetakan bisa saja lebih kompleks dari ini. Beruntungnya, simple-escp menyediakan fasilitas template dalam bentuk JSON. Untuk membuat template JSON, saya men-klik kanan folder src dan memilih menu New, File. Saya kemudian memberi nama file berupa template.json dan men-klik tombol Ok.

Saya mengisi file template.json menjadi seperti berikut ini:

{
     "pageFormat": {
         "pageLength": 10,
         "pageWidth": 50,
         "usePageLengthFromPrinter": false
     },
     "template": {                    
         "header": [ "  PT. XYZ                              HAL: %{PAGE_NO}" ],
         "detail": [ 
             " Ini adalah baris %{LINE_NO} dari halaman %{PAGE_NO} ",
             " ini adalah baris %{LINE_NO} dari halaman %{PAGE_NO} "
         ]
     }
 }

File JSON di simple-escp terdiri atas 2 bagian, pageFormat dan template. Pada pageFormat, saya dapat menentukan pengaturan seperti ukuran halaman (panjang dalam baris dan lebar dalam jumlah karakter). Pada contoh di atas, saya juga memberi nilai usePageLengthFromPrinter dengan false agar ukuran halaman yang tersimpan di RAM printer diabaikan dan ukuran efektif yang berlaku adalah yang telah saya tentukan (10 baris x 50 karakter). Ini akan mempengaruhi posisi akhir ketika menekan tombol Tear Off di printer.

Pada bagian template, saya dapat memakai key berupa firstPage, header, detail, footer, dan lastPage. Pada template di atas, saya hanya mendeklarasikan section header dan detail. Isi dari header akan selalu dicetak di setiap awal halaman.

simple-escp mendukung function dalam syntax seperti %{...}. Pada contoh di atas, %{PAGE_NO} akan diganti menjadi nomor halaman (mulai dari 1) dan %{LINE_NO} akan diganti menjadi nomor baris (mulai dari 1).

Untuk mencetak JSON template di atas, saya dapat membuat kode program seperti berikut ini:

import simple.escp.SimpleEscp;
import simple.escp.Template;
import simple.escp.fill.FillJob;
import simple.escp.json.JsonTemplate;
import simple.escp.util.EscpUtil;

public class Main {

    public static void main(String[] args) {
        try {
            SimpleEscp simpleEscp = new SimpleEscp("EPSON LX-310 ESC/P");
            Template template = new JsonTemplate(Main.class.getResourceAsStream("template.json"));
            String hasil = new FillJob(template.parse()).fill();
            simpleEscp.print(hasil);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

Pada kode program di atas, saya memakai FillJob untuk menerjemahkan JsonTemplate menjadi sebuah String biasa. Setelah itu, saya mencetak String tersebut dengan simpleEscp.print().

Selain function, simple-escp juga mendukung placeholder dalam syntax seperti ${...}. Placeholder dapat dianggap seperti variabel yang akan diganti nilainya berdasarkan sumber data yang dipakai. Sumber data yang dipakai oleh simple-escp ditangani oleh salah satu implementasi interface DataSource. Saat ini, simple-escp dilengkapi dengan BeanDataSource untuk membaca nilai dari sebuah object dan MapDataSource untuk membaca nilai dari sebuah Map.

Sebagai latihan, saya akan mengubah isi file template.json agar memakai placeholder:

{
     "pageFormat": {
         "pageLength": 10,
         "pageWidth": 50,
         "usePageLengthFromPrinter": false
     },
     "template": {
         "header": [ "  PT. XYZ                              HAL: %{PAGE_NO}" ],
         "detail": [
             " Nomor Faktur: ${nomorFaktur:20}",
             {
                "table": "listItemFaktur",
                "border": true,
                "columns": [
                    { "caption": "Nama Barang", "source": "namaBarang", "width": 15, "wrap": true },
                    { "caption": "Qty", "source": "qty::right", "width": 10 },
                    { "caption": "Harga", "source": "harga::right", "width": 20 }
                ]
             }
         ]
     }
 }

Placeholder seperti ${nomorFaktur:20} akan diganti dengan nilai nomorFaktur dari DataSource. Bila nilai
tersebut melebihi batas 20 karakter, maka isinya akan dipotong sehingga nilai dari placeholder tidak akan pernah
lebih dari 20 karakter. Bila kurang dari 20 karakter, maka sisa ruang sisa akan di-isi dengan spasi.

Pada template di atas, saya juga membuat sebuah table yang terdiri atas 3 kolom. Isi dari tabel tersebut akan diambil dari nilai listItemFaktur di DataSource. Nilai dari listItemFaktur wajib harus berupa Collection, misalnya sebuah List. Penggunaan "wrap": true pada kolom akan menyebabkan nilai yang melebihi batas kolom tidak dipotong, melainkan dicetak pada baris berikutnya. Nilai "source" seperti "qty::right" akan menyebabkan nilai dicetak dengan rata kanan.

Berikutnya, karena saya ingin sumber data laporan diambil dari objek, maka saya membuat class Faktur yang isinya seperti berikut ini:

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

public class Faktur {

    private String nomorFaktur;
    private List<ItemFaktur> listItemFaktur = new ArrayList<ItemFaktur>();

    public Faktur(String nomorFaktur) {
        this.nomorFaktur = nomorFaktur;
    }

    public String getNomorFaktur() {
        return nomorFaktur;
    }

    public List<ItemFaktur> getListItemFaktur() {
        return listItemFaktur;
    }

    public void tambahItemFaktur(ItemFaktur itemFaktur) {
        this.listItemFaktur.add(itemFaktur);
    }
}

Class tersebut akan membutuhkan class ItemFaktur yang isinya seperti berikut ini:

import java.math.BigDecimal;

public class ItemFaktur {

    private String namaBarang;
    private Integer qty;
    private BigDecimal harga;

    public ItemFaktur(String namaBarang, Integer qty, BigDecimal harga) {
        this.namaBarang = namaBarang;
        this.qty = qty;
        this.harga = harga;
    }

    public String getNamaBarang() {
        return namaBarang;
    }

    public Integer getQty() {
        return qty;
    }

    public BigDecimal getHarga() {
        return harga;
    }

}

Sekarang, saya dapat mendeklarasikan sebuah objek yang menjadi sumber data seperti berikut ini:

Faktur faktur = new Faktur("FA-1234-556677-XX-BB-CC");
faktur.tambahItemFaktur(new ItemFaktur("Plantronics Backbeat Go 2 With Charging Case",
    1, new BigDecimal("13750000")));
faktur.tambahItemFaktur(new ItemFaktur("CORT Gitar Akustik AD810 - Natural Satin",
    1, new BigDecimal("14900000")));
faktur.tambahItemFaktur(new ItemFaktur("SAMSON Monitor Speaker System MediaOne 3A",
    1, new BigDecimal("14250000")));

Untuk menghasilkan String dan mencetak laporan, saya dapat menggunakan cara singkat seperti berikut ini:

SimpleEscp simpleEscp = new SimpleEscp("EPSON LX-310 ESC/P");            
Template template = new JsonTemplate(Main.class.getResourceAsStream("template.json"));            
simpleEscp.print(template, DataSources.from(faktur));

Factory DataSources akan menghasilkan class DataSource secara otomatis (dalam hal ini adalah BeanDataSource). Bila kode program di atas dijalankan, maka printer akan langsung mencetak tabel sesuai dengan isi dari objek faktur.

Ada kalanya lebih baik memberikan tampilan preview bagi pengguna sebelum mencetak. Untuk kebutuhan ini, simple-escp dilengkapi dengan komponen PrintPreviewPane yang merupakan sebuah JPanel untuk menampilkan preview percetakan. Sebagai latihan, untuk memakai PrintPreviewPane, saya mengubah kode program menjadi berikut ini:

import simple.escp.Template;
import simple.escp.json.JsonTemplate;
import simple.escp.swing.PrintPreviewPane;
import javax.swing.*;
import java.awt.*;
import java.math.BigDecimal;

public class Main extends JFrame {

    public Main() {
        super("Latihan");

        Faktur faktur = new Faktur("FA-1234-556677-XX-BB-CC");
        faktur.tambahItemFaktur(new ItemFaktur("Plantronics Backbeat Go 2 With Charging Case",
                1, new BigDecimal("13750000")));
        faktur.tambahItemFaktur(new ItemFaktur("CORT Gitar Akustik AD810 - Natural Satin",
                1, new BigDecimal("14900000")));
        faktur.tambahItemFaktur(new ItemFaktur("SAMSON Monitor Speaker System MediaOne 3A",
                1, new BigDecimal("14250000")));

        Template template  = null;
        try {
            template = new JsonTemplate(Main.class.getResourceAsStream("template.json"));
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        PrintPreviewPane preview = new PrintPreviewPane(template, null, faktur);
        setLayout(new BorderLayout());
        add(preview, BorderLayout.CENTER);

        pack();
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }

    public static void main(String[] args) {
        new Main();
    }

}

Bila program dijalankan, akan muncul tampilan seperti pada gambar berikut ini:

Tampilan PrintPreviewPanel

Tampilan PrintPreviewPanel

Pengguna kini dapat menentukan sendiri kapan memulai percetakan dengan men-klik tombol Print pada PrintPreviewPane.

Merancang Laporan Master-Detail Tanpa Subreport

Pengenalan

Pada artikel ini, saya akan membuat sebuah laporan master-detail dengan iReport. Sebagai latihan, laporan master-detail yang akan saya buat adalah laporan piutang pelanggan dimana dalam setiap piutang disertakan informasi pembayaran yang telah dilakukan. Laporan akan dikelompokkan berdasarkan setiap pelanggan. Sebagai contoh, berikut ini adalah domain model yang sudah ada pada aplikasi:

Domain model yang akan dipakai dalam laporan

Domain model yang akan dipakai dalam laporan

Karena saya sudah memiliki domain model, maka saya akan memakai instance dari domain model tersebut secara langsung pada laporan.

Langkah pertama yang saya lakukan adalah membuat laporan baru di iReport dengan memilih menu File, New. Saya akan memberinya nama berupa laporan_sisa_piutang. Setelah laporan baru selesai dibuat, saya perlu membuat data source yang mewakili data yang akan ditampilkan dalam laporan nantinya. Tapi sebelumnya, saya memastikan terlebih dahulu bahwa JAR untuk proyek saya sudah terdaftar dengan memilih menu Tools, Options, Classpath.

Menyiapkan Data Source

Saya menambahkan data source baru dengan men-klik tombol Report Datasources seperti pada gambar berikut ini:

Membuat data source baru

Membuat data source baru

Pada dialog yang muncul, saya men-klik tombol New. Saya kemudian memilih JavaBeans set datasource. Saya akan menggunakan kode program Groovy berikut ini untuk menghasilkan data random yang dipakai dalam men-preview laporan:

import domain.penjualan.*
import domain.faktur.*
import org.joda.time.*
import java.util.Random

void tambahFaktur(Konsumen k) {
   Random r = new Random()
   (r.nextInt(10)+1).times {
       FakturJualOlehSales f = new FakturJualOlehSales(tanggal: LocalDate.now(), status: StatusFakturJual.DITERIMA)
       f.nomor = "F${r.nextInt(1000)}"
       def jumlahPiutang = r.nextInt(10000000)
       f.piutang = new KewajibanPembayaran(jumlah: jumlahPiutang)
       def jumlahPembayaran = r.nextInt(10)
       (r.nextInt(10)+1).times {
          Pembayaran p = new Pembayaran(tanggal: LocalDate.now(), 
             jumlah: r.nextInt((int)f.sisaPiutang(false)))
          if (r.nextInt(10) > 5)  {
             p.bilyetGiro = new BilyetGiro(nomorSeri: 'AA-123')
          }
          if (r.nextInt(10) > 8) {
             p.potongan = true
          }
          f.piutang.bayar(p)   
       }

       k.listFakturBelumLunas << f
    }
}

Konsumen k1 = new Konsumen(nama: 'Konsumen A', listFakturBelumLunas: [])
Konsumen k2 = new Konsumen(nama: 'Konsumen B', listFakturBelumLunas: [])
Konsumen k3 = new Konsumen(nama: 'Konsumen C', listFakturBelumLunas: [])

tambahFaktur(k1)
tambahFaktur(k2)
tambahFaktur(k3)

[k1,k2,k3]

Untuk kebutuhan pribadi, saya sempat melakukan modifikasi pada iReport (di https://github.com/JockiHendry/ireport-fork) agar bisa memasukkan kode program Groovy secara langsung seperti pada gambar berikut ini:

Memasukkan kode program pada saat membuat data source

Memasukkan kode program pada saat membuat data source

Bila memakai iReport yang resmi, saya harus membuat kode program Groovy di atas pada proyek yang sudah ada, kemudian menyertakan nama class dan nama method yang mengembalikan Collection di dialog ini.

Saya kemudian men-klik tombol Test untuk memastikan bahwa kode program dapat berjalan secara lancar. Setelah itu, saya men-klik tombol Save.

Berikutnya, saya men-klik tombol Report Query seperti pada gambar berikut ini:

gambar4

Pada dialog yang muncul, saya memilih tab JavaBean Datasource. Kemudian saya mengisi Class name dengan domain.penjualan.Konsumen dan men-klik tombol Read attributes. Karena saya merancang domain model berdasarkan aturan di domain driven design, maka saya hanya perlu men-query root aggregate secara langsung. Pada contoh ini, Konsumen adalah root aggregate yang memiliki informasi piutang dan pembayarannya (dalam bentuk atribut). Oleh sebab itu, saya menambahkan beberapa atribut yang dibutuhkan dengan memilihnya dan men-klik Add selected field(s) seperti pada gambar berikut ini:

Membuat report query

Membuat report query

Setelah selesai, saya men-klik tombol Ok.

Pada laporan ini, terdapat 3 hierarki informasi yang hendak ditampilkan: pelanggan (diwakili class Konsumen), piutang untuk masing-masing pelanggan (diwakili oleh Konsumen.listFakturJualBelumLunas), dan pembayaran untuk masing-masing piutang (diwakili oleh Konsumen.listFakturJualBelumLunas.piutang.listPembayaran). Bila menggunakan fasilitas subreport, saya perlu membuat 3 laporan berbeda dan memanggilnya dalam sebuah laporan. Cara ini cukup merepotkan dan sulit dikelola. Oleh sebab itu, pada artikel ini, saya akan memakai cara yang lebih sederhana tanpa perlu membuat subreport, dengan menggunakan fasilitas List component dan Table component dari JasperReports.

Menampilkan Pelanggan

Ini adalah tugas paling mudah. Saya hanya perlu men-drag beberapa field sehingga menghasilkan rancangan seperti pada gambar berikut ini:

Rancangan laporan untuk pelanggan

Rancangan laporan untuk pelanggan

Bila saya men-preview laporan, yang saya dapatkan hanya informasi pelanggan seperti pada gambar berikut ini:

Hasil preview laporan

Hasil preview laporan

Menampilkan Piutang Untuk Masing-Masing Pelanggan

Untuk menampilkan piutang per pelanggan, saya akan menggunakan List component yang dapat ditemukan di palette seperti berikut ini:

List component

List component

Setelah men-drag komponen tersebut ke laporan, iReport akan membuatkan sebuah dataset baru dengan nama dataset1 yang terlihat seperti pada gambar berikut ini:

Dataset yang dihasilkan iReport

Dataset yang dihasilkan iReport

Saya akan mengubah nama dataset tersebut menjadi dsPiutang dan menambahkan beberapa attribute milik class FakturJualOlehSales sehingga dataset tersebut terlihat seperti pada gambar berikut ini:

Dataset yang sudah diubah

Dataset yang sudah diubah

Berikutnya, saya men-klik kanan pada List component yang ada di laporan dan memilih menu Edit list datasource. Saya kemudian mengisi kotak dialog yang muncul sehingga terlihat seperti pada gambar berikut ini:

Mengisi expression untuk data source

Mengisi expression untuk data source

Setelah itu, saya bisa men-drag field yang dibutuhkan kedalam List component seperti yang terlihat pada gambar berikut ini:

Rancangan laporan dengan List component berisi piutang

Rancangan laporan dengan List component berisi piutang

Sekarang, bila saya men-preview laporan, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil preview laporan

Hasil preview laporan

Menampilkan Pembayaran Untuk Masing-Masing Piutang

Untuk menampilkan daftar pembayaran per piutang/faktur, saya akan menggunakan Table component. Untuk itu, saya men-drag Table component dari palette ke dalam List component yang sudah ada. Table component memiliki icon yang terlihat seperti pada gambar berikut ini:

Table component

Table component

Pada table wizard yang muncul, saya men-klik tombol New dataset. Saya kemudian mengisi nama dataset dengan dsPembayaran, memilih Create an empty dataset, lalu men-klik tombol Finish. Saya kemudian men-klik tombol Next dua kali. Pada step ke-3, saya memilih Use a JRDataSource expression dan mengisi ekspresi-nya dengan:

new net.sf.jasperreports.engine.data.
JRBeanCollectionDataSource($F{piutang}.listPembayaran)

Saya men-klik tombol Next dan melakukan pengaturan seadanya di step ke-4 sebelum akhirnya men-klik tombol Finish.

Selanjutnya, saya menambahkan attribut milik Pembayaran pada dsPembayaran sehingga dataset tersebut terlihat seperti pada gambar berikut ini:

Dataset yang telah diubah

Dataset yang telah diubah

Sekarang saya siap untuk merancang tabel. Agar dapat merancang tabel, saya perlu beralih ke rancangan tabel dengan men-klik Table 1 di bagian bawah layar seperti yang terlihat pada gambar berikut ini:

Mengubah ke modus rancangan tabel

Mengubah ke modus rancangan tabel

Setelah itu, saya membuat rancangan tabel seperti pada gambar berikut ini:

Contoh rancangan tabel

Contoh rancangan tabel

Setelah rancangan selesai, saya dapat kembali halaman utama laporan dengan men-klik tombol Main report di bagian bawah layar. Saya dapat melakukan perubahan lagi agar laporan terlihat rapi. Hasil akhirnya, bila saya men-preview laporan, sekarang laporan akan terlihat seperti:

Hasil preview laporan

Hasil preview laporan

Laporan master-detail berhasil ditampilkan dengan baik cukup dengan satu laporan tunggal tanpa harus melibatkan subreport.

Implementing Aggregate Root In JPA: @OneToMany or @ElementCollection?

In domain driven design, an aggregate root contains one or more entities that represent a bounded context. Those entities should be only manipulated from their aggregate root. In UML class diagram, this is represented as composition (a filled diamond in relationship). Note that UML class diagram also has a concept of aggregation (a hollow diamond in a relationship). Despite similarity in the name, the contained part in UML aggregation can exists without its container. Thus, it is not something like DDD’s aggregate root in which the contained part should not exist without their container.

For example, the following is an UML class diagram with composition:

Composition in UML Class Diagram

Composition in UML Class Diagram

Invoice is the root aggregate that manages LineItem. Every LineItem is a value object. No one should be able to add or delete LineItem directly without obtaining an Invoice first. Because instances of LineItem are value objects, they don’t have a global identity. In the other side, instances of Invoice class are entities so they can be searched by a global identity (for example: invoice number). Each LineItem is associated with a Product entity. This is valid in domain driven design though some people will recommend using value object instead. The value object will store the Product identity (for example: product number). See this article for more information: http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_2.pdf.

The question is how to implement the classes in our diagram using JPA? Well, there are several possibilities with surprising caveats. The recommended way for @OneToMany relationship is bidirectional with owner on the many side. But this violates our aggregate root rules. No one should select existing LineItem or add new LineItem directly! They must manipulate LineItem from Invoice only. This can be solved by using unidirectional @OneToMany with @JoinColumn. But it is still not a containment. To implement the real containment, use @ElementCollection and mark all value objects as @Embeddable. Note that @ElementCollection is introduced in JPA 2.

For example, this is an implementation using @OneToMany and @JoinColumn in Groovy + Hibernate +simple-jpa:

import ...

@DomainClass @Entity @Canonical
class Invoice {

    @NotEmpty
    String number

    @OneToMany @JoinColumn
    List<LineItem> lineItems = []

    public void add(LineItem lineItem) {
        lineItems << lineItem
    }

}


@DomainClass @Entity @Canonical
class LineItem {

    @NotNull @ManyToOne
    Product product

    @NotNull
    Integer quantity

}


@DomainClass @Entity @Canonical
class Product {

    @NotEmpty
    String name

}

The code above will produce the following database tables:

Tables for @OneToMany with @JoinColumn

Tables for @OneToMany with @JoinColumn

Table for LineItem has an identity. This primary key is required for one to many relationships. In our case, it is pretty useless because LineItem should only be identified with their Invoice. The identity of LineItem has no meaning in global context.

This code will create several objects based on our domain classes:

def productA = new Product('Product A')
persist(productA)
def productB = new Product('Product B')
persist(productB)

def invoice = new Invoice('Invoice-01')
invoice.add(new LineItem(productA, 10))
invoice.add(new LineItem(productB, 20))
persist(invoice)

But if you execute the code above, you will get org.hibernate.TransientObjectException!! Every single LineItem must be persisted before persisting Invoice. This is a bit annoying. It doesn’t show that our Invoice is the boss – the aggregate root. To solve this problem, you will need to add @OneToMany(cascade=CascadeType.ALL) to Invoice.lineItems:

...
@OneToMany(cascade=CascadeType.ALL) @JoinColumn
List<LineItem> lineItems = []
...

Now if you run the code, Hibernate will actually perform the following SQL queries:

insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Invoice (createdDate, deleted, modifiedDate, number, id) values (?, ?, ?, ?, ?)
insert into LineItem (createdDate, deleted, modifiedDate, product_id, quantity, id) values (?, ?, ?, ?, ?, ?)
insert into LineItem (createdDate, deleted, modifiedDate, product_id, quantity, id) values (?, ?, ?, ?, ?, ?)
update LineItem set lineItems_id=? where id=?
update LineItem set lineItems_id=? where id=?

Note that Hibernate will issue both insert and update query for every LineItem. So, if I insert 10 LineItem objects, Hibernate will issue 20 queries: 10 insert queries and another 10 update queries. Isn’t this a bit overwhelming?

What happened if I update existing LineItem or insert new LineItem such as shown in the following code:

def invoice = findInvoiceByNumberFetchComplete('Invoice-01')
invoice.lineItems[0].product = findProductByName('Product B')
invoice.lineItems[0].quantity = 999
invoice.lineItems.remove(1)
merge(invoice)

If I run the code, Hibernate will execute the following queries:

...
update LineItem set createdDate=?, deleted=?, modifiedDate=?, product_id=?, quantity=? where id=?
update LineItem set lineItems_id=null where lineItems_id=? and id=?
...

Hibernate only issue two update queries! Even when we remove the second LineItem in our Invoice in the source code, Hibernate doesn’t actually remove it from database. Hibernate only set LineItem.lineItems_id to null so in the next select query, we will not see that second item. To force Hibernate to delete the second item, add orphanRemoval=true to @OneToMany as shown in the following code:

...
@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true) @JoinColumn
List<LineItem> lineItems = []
...

The updated mapping will generate the following queries:

update LineItem set createdDate=?, deleted=?, modifiedDate=?, product_id=?, quantity=? where id=?
update LineItem set lineItems_id=null where lineItems_id=? and id=?
delete from LineItem where id=?

While it is possible to implement aggregate roots and their managed objects using @OneToMany, developers can still manipulate objects directly without their aggregate roots. Our LineItem is required to have an identity in the mapping but we know that value objects shouldn’t have a global identity. Now imagine if you have several genius kids in your team who don’t like to follow your domain driven design rules! They code in whatever direction they want because they think they can!! When the system grows larger, some of the genius kids resigned and new kids join your team. They even do a big refactoring. At the end, you may have a big ball of mud. See http://laputan.org/mud/ for more information about this anti pattern.

To create a more restricted implementation, you can use @ElementCollection. One of the possible implementation using @ElementCollection will be:

@DomainClass @Entity @Canonical
class Invoice {

    @NotEmpty
    String number

    @ElementCollection
    List<LineItem> lineItems = []

    public void add(LineItem lineItem) {
        lineItems << lineItem
    }

}

@Embeddable @Canonical
class LineItem {

    @NotNull @ManyToOne
    Product product

    @NotNull
    Integer quantity

}

@DomainClass @Entity @Canonical
class Product {

    @NotEmpty
    String name

}

Note that LineItem is annotated with @Embeddable not @Entity. In JPA, @Embeddable is used for value object. @Embeddable class doesn’t have an identity just like what a value object should be. But this imposes a limitation: an embeddable class can have collections, but if it is embedded in another embeddab class, it can’t have collections. This is an important limitation if you’re going to implement all managed classes as @Embeddable!

The new domain classes will produce the following tables:

Tables for @ElementCollection and @Embeddable

Tables for @ElementCollection and @Embeddable

To insert new objects, I use the same code as in @OneToMany mapping. While it is the same code, Hibernate now generates different queries:

insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Invoice (createdDate, deleted, modifiedDate, number, id) values (?, ?, ?, ?, ?)
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)

Note that there are no more annoying updates like in @OneToMany mapping. How about update and delete? The same code now will generate the following queries:

delete from Invoice_lineItems where Invoice_id=?
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)

Wait, this is a big difference!! Hibernate will always delete all LineItem records from our Invoice before re-inserting old, updated and new records. Hibernate must do this because our LineItem doesn’t have an identity. Hibernate doesn’t know which records to update or delete if there is no identity on the records.
This behavior is acceptable in small collection. But this is not efficient for a large collection because Hibernate will re-create all records even when only one LineItem is changed. To avoid such case, you can use @OrderColumn in the List, such as:

...
@ElementCollection @OrderColumn
List<LineItem> lineItems = []
...

This new mapping will add a new field to Lineitem table that stores index number (remember that List is a number indexed collection).

Tables for @ElementCollection with @OrderColumn

Tables for @ElementCollection with @OrderColumn

With this mapping, the update and delete code will execute the following queries:

delete from Invoice_lineItems where Invoice_id=? and lineItems_ORDER=?
update Invoice_lineItems set product_id=?, quantity=? where Invoice_id=? and lineItems_ORDER=?

Now, Hibernate will not delete all line item but only delete the second line item (because it was deleted in the following code: invoice.lineItems.remove(1)).