Memperbaharui Output Webpack Di Browser Secara Otomatis

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

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

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

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

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

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

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

> Listening at http://localhost:8080

 75% EXECUTING
> :frontend:runDev

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

 WAIT  Compiling...00:00:12

 DONE  Compiled successfully in 154ms00:00:13

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

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

Iklan

Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama

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

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

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

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

Membuat Proyek Spring Boot Baru

Membuat Proyek Spring Boot Baru

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

Struktur Proyek Spring Boot Yang Baru Dibuat

Struktur Proyek Spring Boot Yang Baru Dibuat

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

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

$ npm install -g vue-cli

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

$ vue init webpack frontend

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

Proyek Vue.js di dalam proyek Spring Boot

Proyek Vue.js di dalam proyek Spring Boot

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

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

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

repositories {
  mavenCentral()
}

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

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

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

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

jar.dependsOn runBuild

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

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

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

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

rootProject.name = 'latihan'
include 'frontend'

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

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

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

Daftar perintah Gradle untuk proyek ini

Daftar perintah Gradle untuk proyek ini

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

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

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

  Build complete.

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

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

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

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

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

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

Bagaimana dengan deployment? Bila saya memberikan perintah:

$ ./gradlew war

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

Memakai Java JSON API Untuk Membaca Facebook Graph API Di Aplikasi Desktop

Java API for JSON Processing (JSR-353) adalah API standar dari Java untuk membaca dan menulis format JSON. JSON (JavaScript Object Notation) adalah format data yang sangat populer di dunia pemograman web. JSON lebih mudah dipakai dan lebih fleksibel bila dibandingkan dengan XML. Karena ia dapat dipakai langsung di JavaScript, pertukaran data pada AJAX sering kali memakai format JSON. Sebelum JSR-353 dirilis pada bulan April 2013, saya harus memakai library pihak ketiga seperti Jackson JSON Processor untuk membaca dan menulis JSON di Java. JSR-353 sekarang merupakan bagian dari Java EE (Enterprise Edition) 7. Bila memakai application server yang mendukung Java EE 7 seperti GlassFish 4, developer dapat membaca dan menulis JSON dengan class yang berada di package javax.json. Catatan: Yang disediakan oleh Java EE 7 hanya API-nya, developer tetap perlu menambahkan implementasi JSR ini.

Pada kesempatan ini, saya tidak akan menggunakan JSR-353 di aplikasi web melainkan di aplikasi desktop. Karena JSR tersebut bukan bagian dari Java SE (Standard Edition), maka saya harus menambahkan API maupun implementasinya secara manual. Karena saya memakai Gradle, maka isi build.gradle di proyek saya akan terlihat seperti berikut ini:

apply plugin: 'java'
apply plugin: 'application'

mainClassName = 'com.wordpress.thesolidsnake.facebookword.Main'

repositories {
    mavenCentral()
}

dependencies {
    compile 'javax.json:javax.json-api:1.0'
    compile 'org.glassfish:javax.json:1.0.4'
}

Saya akan membuat program desktop yang mengakses Facebook Graph API. Dengan Graph API, sebuah program dapat mengakses berbagai informasi dari pengguna Facebook (selama pengguna tersebut memberikan izin). Informasi lebih lanjut mengenai Facebook Graph API dapat dibaca di https://developers.facebook.com/docs/graph-api/. Cara kerja Graph API secara garis besar adalah program mengirim request HTTP ke server Facebook. Kemudian, server Facebook akan mengembalikan hasil dalam format JSON. Dengan demikian, Facebook Graph API bisa disebut sebagai web services yang ‘mirip’ sepertiREST.

Untuk memberikan request HTTP ke server Facebook, saya dapat menggunakan class di package java.net dan javax.net yang merupakan bawaan dari Java SE.

Sebelum memulai memakai Graph API, saya perlu mendapatkan access token untuk user bersangkutan. Karena saya hanya ingin mencoba membaca JSON, maka saya akan mengakses profil pribadi saya. Untuk mendapatkan access token guna keperluan pribadi, saya mengunjungi situs https://developers.facebook.com/tools/explorer. Disini saya men-klik tombol Get Access Token yang terletak di kanan atas. Pada dialog yang muncul, saya memilih Friends Data Permissions dan memberi tanda centang pada friends_status seperti yang terlihat pada gambar berikut ini:

Permission untuk mengakses status friends

Permission untuk mengakses status friends

Setelah itu, saya men-klik tombol Get Access Token. Sekarang saya dapat men-copy paste Access Token yang muncul ke kode program Java.

Sebagai contoh, kode program sederhana berikut akan menampilkan nama user dan id dari si pemilik access token:

package com.wordpress.thesolidsnake.facebookword;

import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonStructure;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class Main {

    public static String ACCESS_TOKEN = "...";  // SESUAIKAN!

    public static JsonStructure getResult(String node, String params) {
        URLConnection cn;
        try {
            URL url = new URL("https://graph.facebook.com/" + node +  "?" + params + "&access_token=" + ACCESS_TOKEN);
            cn = url.openConnection();
            try (InputStream httpInputStream = cn.getInputStream();
                 JsonReader reader = Json.createReader(httpInputStream)) {
                    return reader.read();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        JsonObject identitas = (JsonObject) getResult("me", "fields=id,name");
        System.out.println("JSON = " + identitas);
        System.out.println("Nama = " + identitas.getJsonString("name").getString());
        System.out.println("Id = " + identitas.getJsonString("id").getString());
    }
}

Contoh hasil dari kode program di atas adalah:

JSON = {"name":"Solid Snake","id":"1122334455"}
Nama = Solid Snake
Id = 1122334455

Untuk membaca JSON dari sebuah InputStream, saya menggunakan Json.createReader() yang hasilnya berupa sebuah JsonReader. Disini, saya memiliki beberapa alternatif dalam membaca. Method JsonReader.readArray() akan membaca JSON yang berupa array yang mengembalikan sebuah instance JsonArray. Method JsonReader.readObject() akan membaca JSON yang berupa object yang mengembalikan sebuah intance JsonObject. Bila saya tidak yakin, saya dapat menggunakan method JsonReader.read() yang akan mengembalikan sebuah JsonStructure. Class JsonStructure adalah parent dari JsonObject dan JsonArray. Ia memiliki method getValueType() untuk menentukan jenisnya.

Turunan dari JsonStructure

Turunan dari JsonStructure

Karena bila tidak terjadi kesalahan, hasil kembalian dari pemanggilan Graph API di atas adalah sebuah JSON object, maka saya dengan yakin melakukan casting ke JsonObject. Dari sini, saya dapat memanggil method-method untuk mengakses elemen dalam JsonObject tersebut. Perhatikan bahwa dalam sebuah JsonObject boleh mengandung JsonObject lainnya (nested).

Method pada JsonObject

Method pada JsonObject

Langkah berikutnya, saya akan mencoba mengambil daftar teman Facebook untuk user bersangkutan beserta status mereka. Saya perlu memanggil URL seperti /me/friends?fields=name,statuses.fields(message)&amp;limit=10. Graph API akan mengembalikan JSON dalam format seperti:

{
  "data": [
     {"name": ..., "statuses": { "data": [ ... ], "paging": {...} },
     {"name": ..., "statuses": { "data": [ ... ], "paging": {...} },
     ...
  ],
  "paging": { ... }
}

Untuk membaca format JSON di atas, saya dapat membuat kode program Java seperti berikut ini:

...
System.out.println("\nDaftar Teman:");
JsonObject hasil = (JsonObject) getResult("me/friends",
"fields=name,statuses.fields(message)&limit=10");
JsonArray friends = hasil.getJsonArray("data");
for (JsonObject friend: friends.getValuesAs(JsonObject.class)) {
  System.out.println("Nama = " + friend.getJsonString("name").getString());
  JsonObject hasilStatus = friend.getJsonObject("statuses");
  if (hasilStatus!=null) {
    JsonArray statuses = hasilStatus.getJsonArray("data");
    for (JsonObject status: statuses.getValuesAs(JsonObject.class)) {
      JsonString pesanStatus = status.getJsonString("message");
      if (pesanStatus!=null) {
        System.out.println("\t" + pesanStatus);
      }
    }
  }
}
...

Untuk membaca nilai array dari sebuah JsonObject, saya memanggil method getJsonArray() miliknya yang akan mengembalikan sebuah JsonArray. Class JsonArray mengimplementasikan dua interface, yaitu JsonStructure dan List. Dengan begitu saya dapat memakai class ini sebagai sebuah List yang berisi JsonValue. Agar lebih spesifik, saya dapat memanggil method getValueAs() untuk mendapatkan List dengan tipe data tertentu (selama compatible, tentunya).

Interface yang diimplementasikan JsonArray

Interface yang diimplementasikan JsonArray

Walaupun program di atas sudah bekerja, ada satu hal yang saya abaikan: Graph API memiliki pagination! Setiap object yang dikembalikan memiliki atribut paging yang berupa sebuah object. Bila object paging ini memiliki nilai next, maka terdapat ‘halaman’ atau data berikutnya yang perlu di-proses.

Selain itu, bukankah lebih baik bila output tidak langsung ditampilkan di layar, melainkan disimpan dalam file CSV guna diolah lebih lanjut (misalnya di-sortir melalui Excel)? Oleh sebab itu, saya melakukan perubahan sehingga kode program terlihat seperti berikut ini:

package com.wordpress.thesolidsnake.facebookword;

import javax.json.*;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

public class Main {

    public static String ACCESS_TOKEN = "..."; // UBAH DISINI!

    public static int JUMLAH_TEMAN;
    public static int JUMLAH_STATUS;
    public static StatistikKata STATISTIK;
    static {
        JUMLAH_TEMAN = 0;
        JUMLAH_STATUS = 0;
        STATISTIK = new StatistikKata();
    }

    public static JsonStructure getResult(String node, String params) {
        return getResult("https://graph.facebook.com/" + node + "?" + params + "&access_token=" + ACCESS_TOKEN);
    }

    public static JsonStructure getResult(String urlString) {
        URLConnection cn;
        try {
            URL url = new URL(urlString);
            cn = url.openConnection();
            try (InputStream httpInputStream = cn.getInputStream();
                 JsonReader reader = Json.createReader(httpInputStream)) {
                return reader.read();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static void prosesStatus(JsonObject hasilStatus) {
        if (hasilStatus!=null) {
            JsonArray statuses = hasilStatus.getJsonArray("data");
            for (JsonObject status: statuses.getValuesAs(JsonObject.class)) {
                JsonString pesanStatus = status.getJsonString("message");
                if (pesanStatus!=null) {
                    JUMLAH_STATUS++;
                    STATISTIK.prosesKalimat(pesanStatus.getString());
                }
            }
            if (hasilStatus.get("paging")!=null) {
                JsonString next = hasilStatus.getJsonObject("paging").getJsonString("next");
                if (next!=null) {
                    prosesStatus((JsonObject)getResult(next.getString()));
                }
            }
        }
    }

    public static void prosesFriends(JsonObject hasilFriends) {
        JsonArray friends = hasilFriends.getJsonArray("data");
        for (JsonObject friend: friends.getValuesAs(JsonObject.class)) {
            JUMLAH_TEMAN++;
            STATISTIK.setUserAktif(friend.getJsonString("name").getString());
            System.out.println("Nama = " + friend.getJsonString("name").getString());
            prosesStatus(friend.getJsonObject("statuses"));
        }
        if (hasilFriends.get("paging")!=null) {
            JsonString next = hasilFriends.getJsonObject("paging").getJsonString("next");
            if (next!=null) {
                prosesFriends((JsonObject)getResult(next.getString()));
            }
        }
    }

    public static void main(String[] args) {
        JsonObject identitas = (JsonObject) getResult("me", "fields=id,name");
        System.out.println("Identitas:");
        System.out.println("Nama = " + identitas.getJsonString("name").getString());
        System.out.println("Id = " + identitas.getJsonString("id").getString());

        System.out.println("\nDaftar Teman:");
        JsonObject hasil = (JsonObject) getResult("me/friends", "fields=name,statuses.fields(message)&limit=10");
        prosesFriends(hasil);

        System.out.println("\n");
        System.out.println("====================================");
        System.out.println("Jumlah Teman = " + JUMLAH_TEMAN);
        System.out.println("Jumlah Status = " + JUMLAH_STATUS);
        System.out.println("====================================");

        System.out.println("\n");
        STATISTIK.buatFile("C:\\Users\\User\\Desktop\\statistik.csv");
    }
}

Kode program di atas membutuhkan sebuah class baru, StatistikKata, yang bertugas menghitung jumlah kata, dimana isinya terlihat seperti berikut ini:

package com.wordpress.thesolidsnake.facebookword;

import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Map;

public class StatistikKata {

    public Map data;
    private String userAktif;

    public StatistikKata() {
        data = new HashMap();
    }

    public void setUserAktif(String userAktif) {
        this.userAktif = userAktif;
    }

    public void tambahKata(String kata) {
        if (userAktif==null) throw new RuntimeException("Nama user tidak boleh kosong!");
        Item item = data.get(kata);
        if (item==null) {
            item = new Item();
            data.put(kata, item);
        }
        item.tambah(userAktif);
    }

    public void prosesKalimat(String kalimat) {
        String[] katas = kalimat.split(" ");
        for (String kata: katas) {
            kata = kata.toLowerCase().replaceAll("[^a-zA-Z0-9]", "");
            if (!kata.trim().isEmpty()) {
                tambahKata(kata);
            }
        }
    }

    public void buatFile(String namaFile) {
        try (OutputStream os = Files.newOutputStream(Paths.get(namaFile), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
             PrintWriter pw = new PrintWriter(os)) {

            pw.println("Kata;Jumlah;Jumlah_Teman;Teman");
            for (String key: data.keySet()) {
                Item item = data.get(key);
                pw.printf("%s;%d;%d;%s\n", key, item.getJumlahKata(), item.getJumlahTeman(), item.getDaftarTeman());
            }

            pw.flush();

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    class Item {

        private Map data;

        public Item() {
            data = new HashMap();
        }

        public void tambah(String namaUser) {
            if (data.containsKey(namaUser)) {
                Long jumlah = data.get(namaUser);
                data.put(namaUser, jumlah+1);
            } else {
                data.put(namaUser, 1l);
            }
        }

        public long getJumlahKata() {
            long jumlah = 0;
            for (String key: data.keySet()) {
                jumlah += data.get(key);
            }
            return jumlah;
        }

        public long getJumlahTeman() {
            return data.size();
        }

        public String getDaftarTeman() {
            StringBuffer out = new StringBuffer();
            for (String key: data.keySet()) {
                out.append(",");
                out.append(key);
            }
            return out.substring(1);
        }

    }
}

Sebelum mulai menjalankan program, ada baiknya bila saya menguji terlebih dahulu apakah kode program di class StatistikKata bekerja sesuai harapan. Dengan melakukan unit testing, saya dapat menguji kode program tanpa harus terkoneksi ke internet atau mengakses server Facebook secara langsung. Walaupun membuat unit test terkadang terasa repot, tapi ia dapat membuat saya lebih percaya diri. Biasanya disaat seorang programmer sudah tidak yakin dengan kode programnya tapi ia masih lanjut membuat kode program berikutnya, maka sangat besar kemungkinan ia membuat kesalahan berantai. Sebuah unit test akan membuat sang programmer yakin dengan apa yang barusan dibuatnya (tanpa harus menjalankan keseluruhan program).

Saya akan memakai library JUnit untuk melakukan pengujian, oleh sebab itu saya mengubah build.gradle saya menjadi seperti berikut ini:

...
dependencies {
    ...
    testCompile 'junit:junit:4.11'
}
...

Saya kemudian membuat sebuah file baru src\test\java\com\wordpress\thesolidsnake\facebookword\StatistikKataTest.java yang isinya seperti berikut ini:

package com.wordpress.thesolidsnake.facebookword;

import junit.framework.TestCase;

public class StatistikKataTest extends TestCase {

    public void testProsesKalimat() {
        StatistikKata stat = new StatistikKata();
        stat.setUserAktif("SolidSnake");
        stat.prosesKalimat("Nama saya adalah TheSolidSnake");
        stat.prosesKalimat("Hari ini biasa-biasa saja");
        stat.setUserAktif("LiquidSnake");
        stat.prosesKalimat("Kalau begitu, nama kamu adalah?");

        StatistikKata.Item itemNama = stat.data.get("nama");
        assertEquals(2, itemNama.getJumlahKata());
        assertEquals(2, itemNama.getJumlahTeman());
        assertEquals("SolidSnake,LiquidSnake", itemNama.getDaftarTeman());

        StatistikKata.Item itemSaya = stat.data.get("saya");
        assertEquals(1, itemSaya.getJumlahKata());
        assertEquals(1, itemSaya.getJumlahTeman());
        assertEquals("SolidSnake", itemSaya.getDaftarTeman());

        StatistikKata.Item itemBegitu = stat.data.get("begitu");
        assertEquals(1, itemBegitu.getJumlahKata());
        assertEquals(1, itemBegitu.getJumlahTeman());
        assertEquals("LiquidSnake", itemBegitu.getDaftarTeman());

        StatistikKata.Item itemBiasa = stat.data.get("biasabiasa");
        assertEquals(1, itemBiasa.getJumlahKata());
        assertEquals(1, itemBiasa.getJumlahTeman());
        assertEquals("SolidSnake", itemBiasa.getDaftarTeman());

        StatistikKata.Item itemAdalah = stat.data.get("adalah");
        assertEquals(2, itemAdalah.getJumlahKata());
        assertEquals(2, itemAdalah.getJumlahTeman());
        assertEquals("SolidSnake,LiquidSnake", itemAdalah.getDaftarTeman());

        assertNull(stat.data.get("dia"));
    }

}

Lalu untuk melakukan pengujian, saya mengerjakan task test di Gradle. Bila tidak ada masalah, saya akan menemukan pesan BUILD SUCCESSFUL. Saya juga bisa menemukan laporan HTML di folder build/reports/tests yang isinya akan terlihat seperti berikut ini:

Hasil laporan pengujian

Hasil laporan pengujian

Unit test dalam dijalankan dalam waktu singkat karena unit test tidak mengakses Graph API. Keakuratan sebuah unit test tentu saja ditentukan dari seberapa banyak kombinasi kasus yang diujikan.

Sekarang adalah saatnya menjalankan program dengan menggunakan task run. Program akan membutuhkan waktu lama tergantung pada jumlah teman dan jumlah status mereka serta kecepatan koneksi internet. Setelah selesai, program akan menghasilkan sebuah file berisi daftar kata dan tingkat frekuensinya. Saya kemudian dapat menganalisa file ini dengan membukanya di Excel.

Menciptakan Bahasa Dinamis Di Java: Part 2 – Membuat Back End Dengan ASM

Artikel ini merupakan bagian dari seri artikel tentang pembuatan bahasa dinamis BahasaKu yang berjalan di Java yang terdiri dari:

  1. Membuat Front End Dengan ANTLR
  2. Membuat Back End Dengan ASM
  3. Membuat Operasi Aritmatika
  4. Memanggil Java Libraries

Kode program untuk seri artikel ini dapat ditemukan di https://github.com/JockiHendry/BahasaKu

Back-end di compiler bertugas menerjemahkan teks yang sudah dibaca oleh front-end menjadi target yang diinginkan. Biasanya, target adalah instruksi mesin yang dapat langsung dimengerti oleh CPU. Instruksi mesin tersebut harus dikemas dalam sebuah format file yang dimengerti oleh sistem operasi. Sebagai contoh, biarpun sama-sama memakai prosesor x86 yang sama dengan instruksi mesin yang sama, hasil kompilasi untuk platform Windows dan Linux akan berbeda. Pada sistem operasi Windows, instruksi mesin harus dikemas dalam format Portable Executable dimana pengguna lebih mengenalnya sebagai file EXE. Pada sistem operasi Unix (dan turunannya seperti Linux), instruksi mesin harus dikemas dalam format Executable and Linkable Format. Dengan adanya pemisahan compiler menjadi front-end dan back-end, sebuah front-end yang sama dapat dipakai pada beberapa back-end berbeda sehingga dapat mendukung beberapa platform yang berbeda.

BahasaKu tidak men-target instruksi CPU secara langsung, melainkan ke bytecode Java. Sebagai informasi, setiap hasil kompilasi program Java selalu berada dalam bentuk bytecode bukan instruksi mesin yang dimengeri CPU. Bytecode Java berperan sebagai bahasa perantara sehingga file yang sama dapat berjalan di beberapa platform berbeda. Referensi struktur file class Java dan daftar instruksi bytecode dapat ditemukan di Java Virtual Machine Specification. Secara garis besar, instruksi bytecode lebih sederhana dan lebih terbatas bila dibandingkan dengan instruksi CPU x86. Walaupun demikian, menulis kode program byte per byte (setiap instruksi diwakili oleh sebuah byte) tetap merupakan hal yang melelahkan dan rentan terhadap kesalahan.

Yup! Pada zaman sekarang, dimana sudah bermunculan banyak bahasa pemograman tingkat tinggi, menulis program byte per byte akan terlihat sulit dan aneh. Mungkin itu sebabnya pada zaman dahulu kala, tidak semua orang bisa menjadi programmer ūüôā Beruntungnya, di Java terdapat library ASM. Library ini menyediakan API untuk men-build bytecode secara user friendly dan juga dapat melakukan perhitungan ukuran stack frame secara otomatis. Saya segera mengubah file build.gradle saya menjadi seperti berikut ini (agar Gradle akan men-download ASM bagi saya):

...
dependencies {
    compile group: 'org.antlr', name: 'antlr4', version: '4.1'
    compile group: 'org.ow2.asm', name: 'asm-all', version: '4.2'
}
...

Satu hal yang sering dijumpai dalam bytecode Java adalah type descriptor yang berupa:

  • Z untuk tipe boolean
  • C untuk tipe char
  • B untuk tipe byte
  • S untuk tipe short
  • I untuk tipe int
  • F untuk tipe float
  • J untuk tipe long
  • D untuk tipe double
  • Lnama/package/namaClass; untuk sebuah object. Contoh: Ljava/lang/Object; atau Ljavax/swing/JFrame;
  • Awali dengan tanda [ untuk array. Contoh: int[] diwakili [I dan Object[] diwakili dengan [Ljava/lang/Object;
  • V untuk void

Method descriptor mewakili signature dari sebuah method. Sebagai contoh, signature untuk void main(String[] args) adalah ([Ljava/lang/String;)V. Contoh lainnya, signature untuk int tambah(int angka1, int angka2) adalah (II)I.

Agar code completion di IntelliJ IDEA dapat mengenali class dari ASM, jangan lupa men-klik tombol Refresh Gradle Project di window JetGradle seperti yang terlihat pada gambar berikut ini:

Memperbaharui dependency Gradle

Memperbaharui dependency Gradle

Saya akan mulai dengan membuat sebuah implementasi visitor yang diberi nama BahasaKuVisitor.java di package com.wordpress.thesolidsnake.bahasaku.target.jvm. Dengan menggunakan visitor design pattern, saya akan menelusuri parse tree yang telah dihasilkan oleh front-end. Karena JVM menerima unit kompilasi dalam bentuk class, maka langkah pertama yang saya lakukan adalah membuat sebuah class melalui bytecode, seperti yang terlihat pada kode program berikut ini:

package com.wordpress.thesolidsnake.bahasaku.target.jvm;

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuBaseVisitor;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import org.antlr.v4.runtime.misc.NotNull;
import org.objectweb.asm.*;
import org.objectweb.asm.util.TraceClassVisitor;
import java.io.*;
import java.nio.file.*;
import static org.objectweb.asm.Opcodes.*;

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    private ClassWriter cw;
    private ClassVisitor cv;
    private MethodVisitor mv;
    private Label label0;
    private boolean enableTrace;
    private File sourceFile;
    private String namaSourceFile;

    public BahasaKuVisitor(File sourceFile) {
        this(sourceFile, false);
    }
    public BahasaKuVisitor(File sourceFile, boolean enableTrace) {
        this.sourceFile = sourceFile;
        if (!sourceFile.getName().endsWith(".baku")) {
            throw new RuntimeException("Nama source file [" + sourceFile.getName() + "] tidak diakhiri dengan .baku");
        }
        this.namaSourceFile = sourceFile.getName().substring(0, sourceFile.getName().lastIndexOf('.'));
        this.enableTrace = enableTrace;

        cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        if (enableTrace) {
            cv = new TraceClassVisitor(cw, new PrintWriter(System.out));
        } else {
            cv = cw;
        }

        // Membuat sebuah class public dengan nama (tanpa package) sesuai dengan namaFile
        cv.visit(V1_7, ACC_PUBLIC + ACC_SUPER, namaSourceFile, null, "java/lang/Object", null);

        // Membuat method static void main(String[] args).
        // Seluruh implementasi kode program BahasaKu akan ada di method ini.
        mv = cv.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        mv.visitCode();
        label0 = new Label();
        mv.visitLabel(label0);
    }

}

Pada kode program di atas, saya menggunakan visitor dari ASM untuk membuat bytecode yang mewakili sebuah class. Lagi-lagi visitor design pattern seperti pada ANTLR. Karena proses menghasilkan bytecode sangat rentan terhadap kesalahan, saya menyediakan pilihan untuk memakai TraceClassVisitor yang akan akan mencetak instruksi yang dihasilkan ke layar. Fitur ini akan mempermudah saya untuk mencari kesalahan, tapi umumnya tidak akan begitu berguna bagi programmer yang memakainya.

Hasil kompilasi kode program BahasaKu adalah dalam bentuk sebuah file class (bytecode Java) dengan nama yang sama seperti nama source. Class ini hanya memiliki sebuah method, yaitu public static void main(String[] args) yang didalamnya berisi implementasi kode program BahasaKu. Sangat sederhana sekali, bukan?

Berikutnya, saya akan mulai menerjemahkan setiap grammar satu per satu. Saya akan mulai dari yang paling sederhana, yaitu ekspresi yang mewakili data angka, String dan boolean. Untuk itu saya perlu men-override method visitAngka(), visitString(), visitBooleanTrue() dan visitBooleanFalse() yang dihasilkan ANTLR, seperti pada contoh berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitAngka(@NotNull BahasaKuParser.AngkaContext ctx) {
        Integer angka = Integer.valueOf(ctx.getText());
        mv.visitTypeInsn(NEW, "java/lang/Integer");
        mv.visitInsn(DUP);
        mv.visitIntInsn(BIPUSH, angka);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Integer", "", "(I)V");
        return angka;
    }

    @Override
    public Object visitString(@NotNull BahasaKuParser.StringContext ctx) {
        String str = ctx.getText().substring(1, ctx.getText().length()-1);
        mv.visitLdcInsn(str);
        return str;
    }

    @Override
    public Object visitBooleanTrue(@NotNull BahasaKuParser.BooleanTrueContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;");
        return Boolean.TRUE;
    }

    @Override
    public Object visitBooleanFalse(@NotNull BahasaKuParser.BooleanFalseContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "FALSE", "Ljava/lang/Boolean;");
        return Boolean.FALSE;
    }

    ...

}

Seluruh tipe data pada BahasaKu adalah objek. Pada kode program di atas, saya menerjemahkan angka menjadi objek Integer. Demikian juga, literal iya akan diterjemahkan menjadi Boolean.TRUE dan literal tidak akan diterjemahkan menjadi Boolean.FALSE. Nilai mereka akan di-push ke dalam stack dan siap dipakai oleh perintah yang berada di level di atasnya pada syntax tree.

Berikutnya, saya akan membuat implementasi yang akan menerjemahkan keyword tampilkan menjadi pemanggilan System.out.println() seperti pada berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitTampilkan(@NotNull BahasaKuParser.TampilkanContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visit(ctx.expr());
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V");
        return null;
    }
    ...

}

Walaupun belum membuat implementasi semua rule, saya sudah tidak sabar untuk mencoba memakai BahasaKu. Oleh sebab itu, saya akan membuat sebuah method yang berguna untuk menulis file class (atau output dari compiler BahasaKu) yang terlihat seperti berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    public void generateOutput() {
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        cv.visitEnd();

        Path targetFile = Paths.get(sourceFile.getAbsoluteFile().getParent().toString(), namaSourceFile + ".class");;
        System.out.println("Menghasilkan target file di lokasi: " + targetFile.toString() + "\n");
        try {
            Files.write(targetFile, cw.toByteArray(),
                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (Exception ex) {
            System.out.println("Terjadi kesalahan saat membuat file target!");
            ex.printStackTrace();
        }
    }

    ...

}

Berikutnya, saya membuat sebuah file Main.java di package com.wordpress.thesolidsnake.bahasaku yang isinya seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku;

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuLexer;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        if (args.length==0) {
            System.out.println("Compiler BahasaKu\n\n" +
                "Contoh proses kompilasi:\n" +
                "bahasaku namafilesource.baku\n" +
                "bahasaku source1.baku source2.baku\n" +
                "bahasaku -trace namafilesource.baku\n");
            return;
        }

        boolean enableTrace = false;
        List sourceFiles = new ArrayList();

        // Melakukan parsing argumen
        // Dapat melakukan kompilasi lebih dari satu file
        for (String arg: args) {
            if ("-trace".equals(arg)) {
                enableTrace = true;
            } else {
                sourceFiles.add(arg);
            }
        }

        // Memulai proses kompilasi
        for (String sourceFile: sourceFiles) {
            Path file = Paths.get(sourceFile);
            if (!file.toFile().exists()) {
                System.out.println("Tidak dapat menemukan file: [" + file + "]");
                continue;
            }

            try {

                // Front-end
                ANTLRInputStream antlrInputStream = new ANTLRInputStream(Files.newInputStream(file));
                BahasaKuLexer lexer = new BahasaKuLexer(antlrInputStream);
                CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
                BahasaKuParser parser = new BahasaKuParser(commonTokenStream);
                ParseTree parseTree = parser.file();

                // Back-end
                BahasaKuVisitor jvmVisitor = new BahasaKuVisitor(file.toFile(), enableTrace);
                jvmVisitor.visit(parseTree);
                jvmVisitor.generateOutput();

            } catch (Exception ex) {
                System.out.println("Terjadi kesalahan [" + ex.getMessage() + "]");
                ex.printStackTrace();
            }
        }

    }

}

Class di atas akan mengerjakan lexer dan parser dari ANTLR (front-end) serta menghasilkan file class (back-end). Saya perlu memberi tahu Gradle bahwa class ini adalah class yang harus dijalankan pertama kali dengan menambahkan baris berikut ini pada build.gradle:

...
apply plugin: 'application'
mainClassName = 'com.wordpress.thesolidsnake.bahasaku.Main'
...

Setelah itu, klik tombol Refresh Gradle Project seperti yang ditunjukkan pada gambar 1. Kemudian, saya akan men-double click pada task installApp seperti yang diperlihatkan pada gambar berikut ini:

Menjalankan task installApp

Menjalankan task installApp

Gradle akan membuat folder install dimana di dalamnya terdapat folder bin dan lib, seperti yang terlihat pada gambar berikut ini:

Struktur Folder Yang Dihasilkan

Struktur Folder Yang Dihasilkan

Pada folder lib akan terdapat seluruh jar yang dibutuhkan. Sementara itu, pada folder lib tersedia launcher yang dapat dipakai untuk menjalankan proyek. Launcher Bahasaku.bat dipakai untuk platform Windows, sementara itu platform lainnya dapat memakai launcher Bahasaku.

Sekarang, saya akan mencoba sebuah membuat sebuah program BahasaKu. Sebagai contoh, saya akan membuat sebuah file dengan nama C:\contoh\Latihan.baku yang isinya seperti ini:

tampilkan 'Solid Snake'
tampilkan 10
tampilkan 20
tampilkan 'Percobaan'

Saya dapat men-compile file di atas dengan perintah seperti berikut ini:

Menjalankan Program Yang Memakai BahasaKu

Menjalankan Program Yang Memakai BahasaKu

Saya menjalankan compiler BahasaKu dengan menyertakan -trace supaya saya dapat melihat instruksi yang dihasilkan oleh ASM. Terlihat bahwa dari file Latihan.baku akan tercipta sebuah file Latihan.class. Output dalam bentuk bytecode Java ini dapat dikerjakan oleh JVM dengan menggunakan perintah java sama seperti program Java lainnya. Jadi, ini menunjukkan sebuah fakta bahwa sebuah program untuk Java tidak harus selalu dibuat dalam bahasa pemograman Java. Fakta menarik lainnya adalah karena output BahasaKu adalah sebuah bytecode dan class Java biasa, maka ia dapat dipanggil oleh program Java lainnya (yang memakai bahasa pemograman Java)! Ini memungkinkan terjadinya polyglot programming dimana programmer menggabungkan lebih dari satu bahasa pemograman di proyek yang sama.

Saat ini, compiler BahasaKu masih belum mendukung variabel dan operasi aritmatika. Oleh sebab itu, pada tulisan berikutnya, saya akan men-implementasi-kan rule yang berhubungan dengan variabel dan operator aritmatika.

Menciptakan Bahasa Dinamis Di Java: Part 1 – Membuat Front End Dengan ANTLR

Artikel ini merupakan bagian dari seri artikel tentang pembuatan bahasa dinamis BahasaKu yang berjalan di Java yang terdiri dari:

  1. Membuat Front End Dengan ANTLR
  2. Membuat Back End Dengan ASM
  3. Membuat Operasi Aritmatika
  4. Memanggil Java Libraries

Kode program untuk seri artikel ini dapat ditemukan di https://github.com/JockiHendry/BahasaKu

Hari ini ada yang berbeda dengan editor blog saya: WordPress.com kini mendukung Markdown. Bila sebelumnya saya harus mengetik dengan menggunakan syntax HTML, kini saya dapat menggunakan syntax Markdown yang lebih nyaman dipakai untuk menulis artikel pemograman. Informasi mengenai fasilitas terbaru di WordPress.com ini dapat dibaca di http://en.support.wordpress.com/markdown/. Saya akan merayakannya dengan menulis artikel mengenai pembuatan bahasa pemograman dinamis yang berjalan di Java.

Saat orang-orang berbicara tentang Java, biasanya yang ada dalam benak mereka adalah bahasa pemograman Java. Tapi Java tidak hanya sekedar bahasa pemograman, ia juga adalah nama untuk sebuah mesin virtual (Java Virtual Machine). Pada awalnya memang hanya bahasa pemograman Java yang dapat dijalankan di Java Virtual Machine (JVM). Akan tetapi seiring dengan tren polyglot programming, lahirlah bahasa-bahasa lain untuk JVM seperti Groovy, Scala, JRuby, Jython, dan sebagainya. Sebagai perbandingan, platform .NET juga memiliki banyak bahasa seperti C# dan VB. Salah satu perbedaan utamanya adalah kebanyakan bahasa pemograman untuk Java diciptakan oleh komunitas (dan bersifat open-source), bukan oleh pihak Oracle selaku pemilik Java.

Kenapa mengembangkan bahasa baru yang berjalan di JVM? Salah satu keuntungannya adalah sifat multi-platform Java. Hasil kompilasi bahasa dapat berjalan di seluruh platform yang didukung oleh Java. Selain itu, bahasa baru yang berjalan di JVM dapat mengakses library Java yang sudah ada (mulai dari fungsi matematika, kalender, database, GUI, dsb) tanpa harus membuat semuanya dari awal.

Sebagai latihan, saya akan membuat sebuah bahasa dengan tipe data dinamis yang berjalan di Java (JVM). Bahasa ini tidak akan memiliki fitur yang lengkap karena tujuan utamanya adalah sebagai bahan pembelajaran. Saya akan menyebutnya sebagai BahasaKu.

Tidak seperti pada bahasa pemograman Java dimana seluruh variabel harus dideklarasikan dengan tipe data yang statis, variabel pada bahasa pemograman Bahasaku tidak memiliki tipe data yang statis dan tidak perlu dideklarasikan terlebih dahulu. BahasaKu mendukung empat jenis tipe data, yaitu angka, String, boolean (dengan nilai berupa iya dan tidak), dan object.

Berikut ini adalah contoh hasil rancangan BahasaKu:

  1. Tipe data String harus diapit kutip tunggal seperti 'contoh'.
  2. Sebuah object dapat dibuat dengan keyword buat, misalnya buat javax.swing.JFrame().
  3. Operator yang didukung adalah <- untuk assignment, . (titik) untuk mengakses elemen dari sebuah object, dan operator aritmatika (+, -, *, /).
  4. BahasaKu memiliki operator # yang akan mengabaikan hasil kembalian pemanggilan method dan selalu mengembalikan object yang sedang aktif.
  5. Perintah tampilkan dapat dipakai untuk mencetak sebuah ekspresi ke console.
  6. Masing-masing perintah harus dalam satu baris (tidak perlu ada tanda titik koma).

Contoh kode program BahasaKu akan terlihat seperti berikut ini:

a <- 'Solid Snake'
tampilkan a
a <- 10
tampilkan a
b <- 20
c <- a + b
tampilkan c

Sampai disini, saya sudah berhasil merancang BahasaKu dengan fungsi yang sangat terbatas. Karena ini hanya sebuah latihan, saya tidak terlalu memikirkan kerancuan yang mungkin timbul pada grammar BahasaKu, seperti kombinasi syntax yang mungkin bermakna ganda. Ini dapat menimbulkan permasalahan sama seperti pada kesalahpahaman antar dua manusia karena kerancuan pada kata yang dipakai (sehingga beda persepsi).

Walaupun BahasaKu sudah selesai dirancang, ia tidak akan pernah bisa dipakai oleh programmer bila tidak ada compiler-nya. Sebuah compiler akan menerjemahkan teks yang ditulis oleh programmer menjadi instruksi yang dapat dimengerti oleh mesin. Compiler BahasaKu memiliki beberapa tugas yang secara garis besar adalah:

  1. Lexical analysis: mengubah tulisan yang diketik programmer menjadi token.
  2. Syntactic analysis: memastikan setiap token sesuai dengan aturan bahasa (syntax atau grammar).
  3. Code generation: menghasilkan bytecode yang dapat dijalankan oleh JVM.

Proses lexical analysis dan syntactic analysis sering disebut sebagai front-end. Sementara itu, proses code generation sering disebut sebagai back-end. Tidak peduli apa target akhir dari kompilasi, proses front-end selalu sama. Oleh sebab itu, saya akan mulai dengan membuat front-end terlebih dahulu.

Front-end terdiri atas lexer dan parser. Tugas lexer adalah mengubah deretan huruf dan angka yang diketik oleh programmer menjadi token (sama seperti seorang anak kecil yang merangkai huruf untuk membentuk kata). Contoh token pada bahasa yang saya buat di atas adalah tampilkan (sebuah keyword), <- (sebuah operator), 'contoh' (sebuah String), dan sebagainya. Langkah berikutnya adalah tugas parser yang akan merangkai token tersebut dalam struktur data yang disebut syntax tree atau parse tree (sama seperti seorang anak kecil yang belajar membaca kalimat yang terdiri atas beberapa kata dengan grammar seperti subjek + predikat + object).

Saya akan mulai dengan membuat sebuah proyek baru di IntelliJ IDEA dengan nama BahasaKu di lokasi C:\BahasaKu seperti yang terlihat pada gambar berikut ini:

Membuat Proyek Baru

Membuat Proyek Baru

Agar proyek lebih mudah dikelola, saya akan menggunakan [Gradle] (http://www.gradle.org) yang telah terpasang di sistem saya. Saya kemudian membuat sebuah file build.gradle di proyek dengan isi seperti berikut ini:

apply plugin: 'java'

repositories {
   mavenCentral()
}

dependencies {
   compile group: 'org.antlr', name: 'antlr4', version: '4.1'
}

Pada Gradle script di atas, terlihat bahwa proyek membutuhkan library ANTLR. Gradle akan men-download library ini secara otomatis bagi saya (oleh sebab itu saya perlu terhubung ke internet). Apa itu ANTLR? ANTLR (ANother Tool For Language Recognition) adalah sebuah lexer dan parser generator dengan input berupa file grammar. Penggunaan ANTLR akan sangat mempermudah dan mempersingkat pembuatan front-end compiler. Contoh proyek yang memakai ANTLR adalah Groovy, Jython, Hibernate (untuk query HQL), dan Twitter (untuk search query).

Untuk menjalankan proyek ini, yang perlu saya lakukan adalah memilih menu View, Tool Windows, JetGradle. Setelah itu saya perlu melakukan asosiasi dengan build.gradle di proyek saat ini, lalu memilih tab Tasks, kemudian men-double click pada build. Gradle juga mungkin akan men-download file yang dibutuhkannya dari internet belum belum tersedia di repository lokal, seperti yang terlihat pada gambar berikut ini:

Men-build Proyek

Men-build Proyek

Berikutnya, saya akan membuat sebuah file grammar ANTLR (yang isinya masih kosong) dengan nama BahasaKu.g4 di lokasi src/main/resources seperti yang terlihat pada gambar berikut ini:

Membuat file grammar

Membuat file grammar

Saya juga akan membuat sebuah contoh program yang memakai bahasa pemograman BahasaKu di lokasi src/test/resources dengan nama Contoh.baku yang isinya seperti berikut ini:

a <- 10
b <- 20
c <- a + b
tampilkan c
tampilkan c * 2 + a * 2 - b * 2
nama <- 'Solid Snake'
tampilkan nama
a <- iya
tampilkan a
b <- tidak
tampilkan b
acak <- buat java.util.Random()
tampilkan acak.nextInt()
f <- buat javax.swing.JFrame()
f.setSize(100,100)#setDefaultCloseOperation(3)#setVisible(iya)

Saat ini saya sudah dapat langsung mengetik isi file grammar di file BahasaKu.g4. Akan tetapi, agar prosesnya menjadi lebih nyaman, saya akan menggunakan sebuah IDE terpisah khusus untuk ANTLR, yaitu ANTLRWorks 2.1 yang dapat di-download di http://tunnelvisionlabs.com/products/demo/antlrworks. Tool berbasis NetBeans Platform ini menyediakan syntax highlightning, code completion dan kemudahan pengujian file grammar.

setelah men-download dan men-extract ANTLRWorks 2.1, saya dapat menjalankannya dengan men-double click file antlrworks2.exe di folder antlrworks2\bin. Kemudian, saya memilih menu File, Open, dan men-browse file BahasaKu.g4 yang ada di lokasi src/main/resources. Saya kemudian mengisi file grammar tersebut sehingga terlihat seperti berikut ini:

grammar BahasaKu;

//
// Parser Rules
//

file
    : (statement NEWLINE)* statement NEWLINE?
    ;

statement
    : tampilkan 
    | expr
    | assignment     
    ;

tampilkan
    : TAMPILKAN expr
    ;

assignment
    : IDENTIFIER ASSIGNMENT expr  
    ;

exprList
    : expr (KOMA expr)* 
    ;

arguments
    : KURUNG_BUKA exprList? KURUNG_TUTUP
    ;

expr
    :   expr op=(TITIK|SELF) IDENTIFIER arguments       # PanggilMethod    
    |   BUAT qualifiedName arguments                    # BuatObject
    |   expr KALI expr                                  # Perkalian
    |   expr BAGI expr                                  # Pembagian
    |   expr TAMBAH expr                                # Penjumlahan
    |   expr KURANG expr                                # Pengurangan    
    |   ANGKA                                           # Angka                                 
    |   BOOLEAN_TRUE                                    # BooleanTrue
    |   BOOLEAN_FALSE                                   # BooleanFalse
    |   STRING                                          # String
    |   IDENTIFIER                                      # Identifier                                                  
    ;

qualifiedName
    : IDENTIFIER ('.' IDENTIFIER)* 
    ;


//
// Lexer Rules
//

TAMPILKAN: 'tampilkan' ;

BUAT: 'buat' ;

BOOLEAN_TRUE: 'iya' ;

BOOLEAN_FALSE: 'tidak' ;

ASSIGNMENT: '<-' ;

KOMA: ',' ;

TITIK: '.' ;

SELF: '#' ;

TAMBAH: '+' ;

KURANG: '-' ;

KALI: '*' ;

BAGI: '/' ;

KURUNG_BUKA: '(' ;

KURUNG_TUTUP: ')' ;

STRING: '\'' .*? '\'' ;

IDENTIFIER: [a-zA-Z$_] [a-zA-Z0-9$_]* ;   

ANGKA:  [0-9]+ ;

NEWLINE:    '\r'? '\n' ;

WS  :   [ \t\r\n]+ -> skip ;

File grammar di atas mewakili aturan yang telah saya buat untuk BahasaKu sebelumnya. Terlihat bahwa saya menggabungkan rule untuk lexer dan parser pada file yang sama. Mereka dapat dipisah ke file yang berbeda untuk meningkatkan reusability.

Pada ANTLR, urutan definisi rule adalah hal yang penting. Sebagai contoh, di rule expr, aturan untuk perkalian dan pembagian muncul lebih dahulu sebelum penjumlahan dan pengurangan. Hal ini menyebabkan di back-end nanti, perkalian dan pembagian akan dikerjakan terlebih dahulu. Begitu juga untuk lexer, token yang lebih terspesialisasi seperti 'tampilkan' dan 'buat' harus muncul lebih dahulu sebelum token identifier (yang mewakili nama variabel dan nama method).

Rule seperti expr (yang mewakili ekspresi) dapat memiliki banyak alternatif. Oleh sebab itu, saya menggunakan label seperti # PanggilMethod, # BuatObject, dsb supaya ANTLR menghasilkan method terpisah (dengan nama yang sama sepeti pada label) sehingga mempermudah saya dalam membuat back-end untuk masing-masing jenis ekspresi tersebut.

Berikutnya, saya akan menguji apakah definisi grammar BahasaKu.g4 sesuai dengan contoh program BahasaKu yang sudah saya buat sebelumnya. Untuk itu, saya memilih menu Run, Run in TestRig…. Pada dialog yang muncul, saya men-browse ke lokasi contoh file program Contoh.baku, seperti yang terlihat pada gambar berikut ini:

Menguji Grammar

Setelah itu, saya men-klik tombol Finish. ANTLR akan menampilkan parse tree inspector seperti yang terlihat pada gambar berikut ini:

Visualisasi Parse Tree

Visualisasi Parse Tree

Bila tidak ada tulisan berwarna merah, maka itu berarti ANTLR dapat membaca source code BahasaKu dengan baik. ANTLR akan menyimpan hasil parsing-nya dalam bentuk struktur data tree atau lebih dikenal sebagai parse tree. Nantinya, di back-end, saya akan menelusuri tree ini dengan memakai pola visitor.

Mengapa ANTLR dan para pembuat compiler lebih senang menyimpan hasil parsing dalam bentuk tree? Mengapa tidak dalam array atau list, misalnya? Hal ini karena di back-end nanti, untuk menerjemahkan syntax menjadi instruksi mesin (atau bytecode Java), dibutuhkan navigasi ‘vertikal’ yang sangat sulit dicapai bila tidak menggunakan tree.

Langkah berikutnya adalah menghasilkan kode program parser dan lexer berdasarkan file grammar yang telah diuji sebelumnya. Saya memulai dengan memilih menu Run, Generate Recognizer…. Saya mengisi dialog yang muncul dengan informasi seperti berikut ini (bila lokasi output directory belum ada, saya perlu membuatnya terlebih dahulu):

Men-generate visitor berdasarkan grammar

Men-generate visitor berdasarkan grammar

Setelah itu, saya men-klik tombol Next. Kali ini saya dapat memilih untuk menghasilkan listener, visitor, atau keduanya. listener lebih mudah dipakai tetapi kurang fleksibel karena tidak dapat mengubah alur eksekusi. Bila memakai visitor, developer dapat mengubah atau mengabaikan alur eksekusi secara bebas. Secara umum, listener lebih tepat dipakai untuk membaca file source dengan alur yang pasti seperti membuat program yang menerjemahkan XML menjadi JSON (dan sebaliknya). Saya lebih sehingga menggunakan visitor sehingga saya memberikan tanda centang pada checkbox tersebut. Tidak lupa saya juga mengisi package untuk file yang dihasilkan seperti pada gambar berikut ini:

Men-generate visitor berdasarkan grammar

Men-generate visitor berdasarkan grammar

Setelah itu, saya men-klik tombol Next dan Finish.

Kembali ke project di IntelliJ IDEA, saya akan menemukan file baru dihasilkan oleh ANTLR seperti yang terlihat pada gambar berikut ini:

Kode Program Yang Di-Generate

Kode Program Yang Di-Generate

Sampai disini, front-end sudah selesai dibuat dan saya bisa melanjutkan ke tahap back-end.

Salah satu hal yang diperhatikan bila memakai ANTLR adalah setiap kali saya mengubah file grammar BahasaKu.g4, maka saya harus men-generate ulang visitor yang ada. Pada langkah di atas, saya melakukannya melalui ANTLRWorks. Sebuah solusi yang lebih otomatis adalah dengan menambahkan script pada build.gradle sehingga file akan digenerate secara otomatis setiap kali proyek dijalankan. Gradle memiliki plugin bawaan untuk ANTLR, namun sayangnya hanya mendukung hingga ANTLR 2. Oleh sebab itu, saya akan mengubah isi build.gradle menjadi seperti berikut ini:

apply plugin: 'java'

ext {
    antlrPackage = 'com.wordpress.thesolidsnake.bahasaku.grammar'
    antlrTargetDir = "src/main/java/${antlrPackage.replace('.','/')}"
    antlrGrammar = 'src/main/resources/BahasaKu.g4'
}

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.antlr', name: 'antlr4', version: '4.1'
}

task antlr4generate(type: JavaExec) {
    description 'Menghasilkan Visitors Dari ANTLR 4'
    main 'org.antlr.v4.Tool'
    classpath project.configurations.runtime
    args '-o', antlrTargetDir
    args '-no-listener'
    args '-visitor'
    args '-package', antlrPackage
    args antlrGrammar
}

compileJava {
    dependsOn antlr4generate
}

Sekarang, setiap kali saya men-compile file Java (menjalankan program), maka ANTLR akan menghasilkan file visitor terbaru untuk saya. Selain itu, saya juga dapat mengerjakan target antlr4generate secara manual seperti yang terlihat pada gambar berikut ini:

Men-generate Visitor ANTLR Langsung Dari IDE

Men-generate Visitor ANTLR Langsung Dari IDE

Memakai Gradle Untuk Mem-build Griffon Dari Source

Selama ini saya memakai Maven yang sudah mature. ¬† Dan kini, bertambah satu lagi tool untuk manajemen proyek yang saya temukan, yaitu Gradle. ¬† Salah satu proyek yang menggunakan Gradle adalah framework Griffon. ¬†Contoh lain, misalnya proyek Android SDK ¬†yang ¬†beralih memakai¬†Gradle. ¬†Sejujurnya, saya lebih memilih Maven sebagai satu-satunya produk manajemen proyek yang diterapkan secara universal. ¬† Tapi bila proyek yang ingin saya modifikasi tidak mendukung Maven, maka cara tergampang mungkin adalah dengan mengikuti ‘arus’ kehendak si pembuat proyek ūüôā

Apa kelebihan Gradle? Gradle tidak memakai XML melainkan memakai Groovy sebagai DSL (Domain Specific Language)-nya. Dengan demikian, saya bisa menyertakan ‘kode program‘ di Gradle. ¬† Penggunaan Groovy di script Gradle bahkan memungkinkan pengguna untuk membuat task secara dinamis, sebuah hal yang sulit dicapai dengan hanya berbekal XML. ¬† Gradle juga dapat memanggil Ant task bila diperlukan. ¬† Gradle tetap dapat memakai repository Maven yang saat ini sudah sangat banyak dipakai.

Untuk memakai Gradle, saya dapat men-download-nya di www.gradle.org/downloads.   Saya akan men-extract folder yang ada di dalam file Zip tersebut.   Setelah itu, saya mengatur environment variable GRADLE_HOME agar berisi lokasi folder tersebut.   Tidak lupa, saya juga menambahkan lokasi %GRADLE_HOME%\bin ke dalam environment variable PATH.

Untuk menguji apakah Gradle ter-install dengan baik, saya membuka Command Prompt dan memberikan perintah seperti berikut ini:

C:\>gradle -version

------------------------------------------------------------
Gradle 1.3
------------------------------------------------------------

...

Tidak lama kemudian, saya men-pull (mengambil kode program) Griffon dari GitHub.   Saya bisa melihat informasi mengenai proyek tersebut dengan masuk (baca: cd) ke lokasi folder yang berisi kode program Griffon, lalu memberikan perintah seperti ini:

C:\projects\griffon-1-2.0>gradle -q projects

------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'griffon'
+--- Project ':griffon-cli'
+--- Project ':griffon-guide'
+--- Project ':griffon-resources'
+--- Project ':griffon-rt'
+--- Project ':griffon-scripts'
+--- Project ':griffon-shell'
\--- Project ':griffon-wrapper'

To see a list of the tasks of a project, run gradle :tasks
For example, try running gradle :griffon-cli:tasks

Untuk melihat task yang dapat dipanggil, saya memberikan perintah seperti berikut ini:

C:\projects\griffon-1.2.0>gradle -q tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend
on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles the main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles the test classes.

Documentation tasks
-------------------
...

Help tasks
----------
...

IDE tasks
---------
cleanEclipse - Cleans all Eclipse files.
cleanIdea - Cleans IDEA project files (IML, IPR)
eclipse - Generates all Eclipse files.
idea - Generates IDEA project files (IML, IPR, IWS)

Installation tasks
------------------
izPackCreateInstaller - Creates an IzPack-based installer

Upload tasks
------------
uploadArchives - Uploads all artifacts belonging to configuration ':griffon-cli:
archives'

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

Other tasks
-----------
...

Rules
-----
...

Untuk melihat ketergantungan proyek, misalnya untuk melihat dependency subproyek griffon-scripts, saya memberikan perintah seperti berikut ini:

C:\projects\griffon-1.2.0>gradle -q dependencies griffon-scripts:dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

default - Configuration for default artifacts.
No dependencies

izpack - The IzPack standalone compiler libraries to be used for this project.
No dependencies

------------------------------------------------------------
Project :griffon-scripts
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

checkstyle - The Checkstyle libraries to be used for this project.
No dependencies

clover
\--- com.cenqua.clover:clover:3.1.2

cobertura
+--- net.sourceforge.cobertura:cobertura:1.9.4.1
|    +--- oro:oro:2.0.8
|    +--- asm:asm:3.0
|    +--- asm:asm-tree:3.0
|    |    \--- asm:asm:3.0 (*)
|    +--- log4j:log4j:1.2.9
|    \--- org.apache.ant:ant:1.7.0
|         \--- org.apache.ant:ant-launcher:1.7.0
\--- junit:junit:4.10
     \--- org.hamcrest:hamcrest-core:1.1

codenarc - The CodeNarc libraries to be used for this project.
No dependencies

compile - Classpath for compiling the main sources.
+--- org.codehaus.griffon:griffon-rt:1.2.0
|    +--- org.codehaus.groovy:groovy-all:2.0.6
|    +--- log4j:log4j:1.2.17
|    +--- org.slf4j:slf4j-log4j12:1.7.2
|    +--- org.slf4j:slf4j-api:1.7.2
|    +--- org.slf4j:jcl-over-slf4j:1.7.2
|    |    \--- org.slf4j:slf4j-api:1.7.2 (*)
|    \--- org.slf4j:jul-to-slf4j:1.7.2
+--- org.codehaus.griffon:griffon-cli:1.2.0
|    +--- org.codehaus.griffon:griffon-rt:1.2.0 (*)
|    +--- org.codehaus.griffon:griffon-resources:1.2.0
|    +--- junit:junit:4.10
|    |    \--- org.hamcrest:hamcrest-core:1.1
|    +--- asm:asm:3.2
|    +--- commons-cli:commons-cli:1.2
|    +--- commons-io:commons-io:2.4
|    +--- commons-lang:commons-lang:2.6
|    +--- commons-collections:commons-collections:3.2.1
|    +--- commons-codec:commons-codec:1.6
|    +--- org.apache.httpcomponents:httpcore:4.1.2
|    +--- com.jcraft:jzlib:1.1.1
|    +--- xml-resolver:xml-resolver:1.2
|    +--- org.fusesource.jansi:jansi:1.9
|    +--- jline:jline:0.9.94
|    |    \--- junit:junit:3.8.1 -> 4.10 (*)
|    +--- org.yaml:snakeyaml:1.9
|    +--- radeox:radeox:1.0-b2
|    +--- org.apache.ant:ant-launcher:1.8.4
|    +--- biz.aQute:bndlib:1.50.0
|    +--- org.apache.ant:ant:1.8.4
|    +--- org.apache.ant:ant-junit:1.8.4
|    +--- org.springframework:org.springframework.core:3.2.0.RELEASE
|    +--- org.springframework:org.springframework.beans:3.2.0.RELEASE
|    +--- org.springframework:org.springframework.context:3.2.0.RELEASE
|    +--- org.springframework:org.springframework.context.support:3.2.0.RELEASE
|    +--- org.xhtmlrenderer:core-renderer:R8
|    +--- com.lowagie:itext:2.0.8
|    +--- org.grails:grails-docs:2.2.0
|    |    +--- org.yaml:snakeyaml:1.8 -> 1.9 (*)
|    |    +--- org.grails:grails-gdoc-engine:1.0.1
|    |    |    \--- org.slf4j:jcl-over-slf4j:1.6.1 -> 1.7.2 (*)
|    |    +--- commons-lang:commons-lang:2.6 (*)
|    |    \--- org.slf4j:jcl-over-slf4j:1.6.2 -> 1.7.2 (*)
|    +--- org.grails:grails-gdoc-engine:1.0.1 (*)
|    +--- org.apache.ivy:ivy:2.2.0
|    +--- org.codehaus.gant:gant_groovy2.0:1.9.8
|    +--- org.codehaus.groovy.modules.http-builder:http-builder:0.5.2
|    +--- net.sf.json-lib:json-lib:2.4
|    +--- net.sf.ezmorph:ezmorph:1.0.6
|    +--- commons-beanutils:commons-beanutils:1.8.3
|    +--- org.apache.httpcomponents:httpclient:4.1.2
|    +--- com.jcraft:jsch:0.1.48
|    +--- xerces:xercesImpl:2.9.1
|    \--- org.codehaus.groovy:groovy-all:2.0.6 (*)
+--- org.codehaus.griffon:griffon-resources:1.2.0 (*)
\--- org.codehaus.groovy:groovy-all:2.0.6 (*)

...

Bagian penting dari sebuah proyek yang memakai Gradle adalah file script yang bernama build.gradle (setara dengan pom.xml di Maven). Pada bagian paling awal dari build.gradle milik source code Griffon, saya akan menemukan baris seperti berikut ini:

import java.text.SimpleDateFormat
import org.apache.ivy.plugins.resolver.URLResolver

Yup, terlihat sangat mirip kode program, bukan? Karena script ini memang adalah bentuk khusus dari kode program Groovy; bukan XML seperti di Maven.

Setelah itu, saya akan menemukan baris seperti berikut ini:

apply plugin: 'base'
apply plugin: 'idea'
apply plugin: 'eclipse'

Baris di atas menunjukkan bahwa apa saja plugin yang dipakai oleh build.gradle ini.   Plugin base menambahkan beberapa tasks untuk membuat dan menghasilkan file arsip dari proyek.   Plugin idea dan eclipse memungkinkan Gradle untuk menghasilkan file proyek IntelliJ IDEA dan Eclipse.   Beberapa cotnoh software development plugins lain adalah checkstyle, codenarc, eclipse-wtp, findbugs, jdepend, pmd, project-report, signing, dan sonar.

Setelah itu, saya akan menemukan baris seperti berikut ini:

evaluationDependsOn(':griffon-rt')
evaluationDependsOn(':griffon-cli')
evaluationDependsOn(':griffon-resources')
evaluationDependsOn(':griffon-scripts')
evaluationDependsOn(':griffon-shell')
evaluationDependsOn(':griffon-wrapper')
evaluationDependsOn(':griffon-guide')

evaluationDependsOn() dipakai untuk menandakan configuration dependencies, dimana file build.gradle yang ada di subproyek griffon-crt, griffon-cli, griffon-resources, griffon-scripts, griffon-shell, griffon-wrapper, dan griffon-guide akan di-evaluasi terlebih dahulu secara berurutan.

Subproyek? Yup!   Proyek Griffon merupakan sebuah contoh yang memakai multi-project builds di Gradle.   Bila saya membuka file settings.gradle, saya akan menemukan isinya seperti berikut ini:

include 'griffon-rt'
include 'griffon-cli'
include 'griffon-resources'
include 'griffon-scripts'
include 'griffon-wrapper'
include 'griffon-guide'
include 'griffon-shell'

rootProject.name = 'griffon'
rootProject.children.each {project ->
    String fileBaseName = project.name
    String projectDirName = "subprojects/$fileBaseName"
    project.projectDir = new File(settingsDir, projectDirName)
    project.buildFileName = "${fileBaseName}.gradle"
    assert project.projectDir.isDirectory()
    assert project.buildFile.isFile()
}

Secara sederhana, file tersebut akan mendaftarkan subproyek yang ada, dimana setiap subproyek berada di dalam folder subprojects.   Masing-masing subproyek memiliki file konfigurasi sesuai dengan nama subproyek tersebut, misalnya griffon-cli.gradle, griffon-guide.gradle, griffon-resources.gradle, dan sebagainya.

Kembali ke build.gradle yang ada di root project, saya akan menemukan baris seperti berikut ini:

subprojects { subproj ->
    apply plugin: 'idea'
    if(plugins.hasPlugin('java')){
        sourceCompatibility = 1.5
        targetCompatibility = 1.5
        group = 'org.codehaus.griffon'

        apply from: "${rootDir}/gradle/coverage.gradle"
        apply from: "${rootDir}/gradle/codeQuality.gradle"
    }

    repositories {
        mavenRepo name: 'Codehaus',       url: 'http://repository.codehaus.org'
        mavenRepo name: 'SpringSource',   url: 'http://repository.springsource.com/maven/bundles/release'
        mavenRepo name: 'Gradle',         url: 'http://gradle.artifactoryonline.com/gradle/libs-releases-local'
        mavenCentral()
        mavenRepo name: 'Sonatype',       url: 'http://repository.sonatype.org/content/groups/public'
        mavenRepo name: 'Grails Central', url: 'http://repo.grails.org/grails/core/'
    }
}

Bagian tersebut akan dikerjakan untuk seluruh subproyek yang ada.

Berikutnya, saya menemukan bagian seperti berikut ini:

Date buildTimeAndDate = new Date()
ext {
    buildTime = new SimpleDateFormat('dd-MMM-yyyy').format(buildTimeAndDate)
    buildDate = new SimpleDateFormat('hh:mm aa').format(buildTimeAndDate)
}

Ini adalah salah satu metode untuk memberikan variabel global di Gradle (melalui extra properties).

Lalu terdapat bagian seperti berikut ini:

configure(mavenizedProjects()) { proj ->
    proj.apply from: "${rootDir}/gradle/maven.gradle"
    proj.task('checkManifest') {
        dependsOn proj.classes
        doLast {
            proj.tasks.withType(Jar).each { jarfile ->
                jarfile.manifest {
                    attributes(
                        'Built-By': System.properties['user.name'],
                        'Created-By': System.properties['java.version'] + " (" + System.properties['java.vendor'] + " " + System.getProperty("java.vm.version") + ")",
                        'Build-Date': buildTime,
                        'Build-Time': buildDate,
                        'Specification-Title': proj.name,
                        'Specification-Version': project.version,
                        'Specification-Vendor': 'griffon-framework.org',
                        'Implementation-Title': proj.name,
                        'Implementation-Version': project.version,
                        'Implementation-Vendor': 'griffon-framework.org'
                    )
                }
            }
        }
    }
    proj.jar.dependsOn proj.checkManifest
}

Closure pada method configure() hanya akan diberlakukan pada subproyek yang diberikan sebagai parameternya.   Lalu, apa isi parameter mavenizedProjects() pada kode program di atas?  Saya dapat menemukannya di baris paling terakhir dari build.gradle:

def mavenizedProjects() {
    [
        project(':griffon-rt'),
        project(':griffon-cli'),
        project(':griffon-resources'),
        project(':griffon-scripts'),
        project(':griffon-shell')
    ]
}

Sekarang, saya akan mencoba melihat isi file konfigurasi untuk salah satu subproyek yang ada, yaitu griffon-cli. Saya akan menemukan baris seperti berikut ini di file griffon-cli.gradle (terletak di folder subprojects/griffon-cli):

apply plugin: 'groovy'
...
dependencies {
    groovy "org.codehaus.groovy:groovy-all:$groovyVersion"

    compile project(':griffon-rt'),
            project(':griffon-resources')
    compile 'junit:junit:4.10',
            'asm:asm:3.2',
            'commons-cli:commons-cli:1.2',
            'commons-io:commons-io:2.4',
            'commons-lang:commons-lang:2.6',
            'commons-collections:commons-collections:3.2.1',
            'commons-codec:commons-codec:1.6',
            'org.apache.httpcomponents:httpcore:4.1.2',
            'com.jcraft:jzlib:1.1.1',
            'xml-resolver:xml-resolver:1.2',
            'org.fusesource.jansi:jansi:1.9',
            'jline:jline:0.9.94',
            'org.yaml:snakeyaml:1.9',
            'radeox:radeox:1.0-b2',
            "org.apache.ant:ant-launcher:$antVersion",
            'biz.aQute:bndlib:1.50.0'
    ...
}

Method dependencies() menentukan apa saja yang dibutuhkan untuk bisa men-build subproyek griffon-cli.   groovy dan compile adalah salah satu configuration yang dapat dipakai bila subproyek mendaftarkan plugin groovy (atau plugin bahasa lainnya).

Bagian compile project(':griffon-rt'), project(':griffon-resources') menunjukkan bahwa subproyek griffon-cli bergantung pada subproyek griffon-rt dan griffon-resources.   Selain ketergantungan pada proyek, Gradle juga memungkinkan melakukan definisi ketergantungan terhadap file lain dengan files() atau isi pada folder dengan fileTree().

Kemana Gradle akan mencari dependencies yang dibutuhkan?   Gradle dapat mencari di repository Maven, Ivy, atau sebuah direktori yang ditentukan.   Bila saya kembali memperhatikan isi method subprojects() di build.gradle, saya akan menemukan lokasi pencarian, seperti yang terlihat berikut ini:

subprojects { subproj ->
    ...
    repositories {
        mavenRepo name: 'Codehaus',       url: 'http://repository.codehaus.org'
        mavenRepo name: 'SpringSource',   url: 'http://repository.springsource.com/maven/bundles/release'
        mavenRepo name: 'Gradle',         url: 'http://gradle.artifactoryonline.com/gradle/libs-releases-local'
        mavenCentral()
        mavenRepo name: 'Sonatype',       url: 'http://repository.sonatype.org/content/groups/public'
        mavenRepo name: 'Grails Central', url: 'http://repo.grails.org/grails/core/'
    }
}

Setelah memahami DSL yang dipakai Gradle, langkah berikutnya adalah men-import proyek Griffon agar dapat dipakai di IntelliJ IDEA.   Cara yang paling gampang adalah dengan membuka IntelliJ IDEA, memilih Import Project, kemudian pilih file build.gradle yang berada dalam folder griffon-1.2.0, kemudian men-klik tombol Next.   IntelliJ IDEA akan menampilkan struktur proyek yang ada disertai dengan pilihan untuk melakukan perubahan bila perlu.  Klik tombol Finish untuk selesai.

Struktur proyek yang dihasilkan berdasarkan file build.gradle Griffon akan terlihat seperti berikut ini:

Struktur Proyek Griffon

Struktur Proyek Griffon

IntelliJ IDEA juga memiliki sebuah window khusus bernama JetGradle project khusus untuk proyek Gradle, seperti yang terlihat pada gambar berikut ini:

Tampilan JetGradle Projects

Tampilan JetGradle Projects

Bagaimana cara menjalankan proyek?  Proyek Griffon tidak dijalankan, melainkan harus di-copy ke sebuah folder yaitu %GRIFFON_HOME% untuk dipakai oleh proyek lain.   Jika saya membuka file package.gradle di folder gradle, saya akan menemukan definisi task seperti berikut ini:

task installBinary(type: Sync, dependsOn: checkGriffonHome) {
    description = 'Installs the binary distribution at $GRIFFON_HOME.'
    destinationDir = griffonHomeDir as File
    with distBinSpec
    doLast {
        ant.chmod(dir: "${griffonHomeDir}/bin", excludes: '*.bat', perm: 'ugo+x')
    }
}

Dengan demikian, yang perlu saya lakukan setelah  melakukan perubahan kode program Griffon adalah memanggil task installBinary untuk men-copy hasil perubahan ke folder %GRIFFON_HOME% (sehingga seluruh proyek lain yang memakai framework Griffon akan memakai kode program Griffon hasil perubahan).

Untuk menjalankan perintah Gradle di IntelliJ IDEA, saya men-klik kanan file build.gradle kemudian memilih Create ‘build’…¬† ¬†Pada dialog yang muncul, saya mengisi Script parameters dengan installBinary, dan men-klik tombol OK. Sekarang, saya bisa men-klik icon Run atau menekan Shift+10 untuk mengerjakan task installBinary di IntelliJ IDEA.

Tapi ternyata tidak segampang ini! Build pertama gagal..   Berikut ini adalah pesan kesalahan yang saya peroleh:

Execution failed for task ':griffon-guide:java2htmlGenerateOverview'.
> Illegal/unsupported escape sequence near index 3
  C:\Projects\griffon-1.2.0\subprojects\griffon-guide\build\java2html
     ^

Saya sepertinya familiar dengan bug ini (kesalahan yang sama dengan yang saya temui di plugin CloudFoundry untuk Eclipse?!) ¬†Di Windows, pemisah direktori adalah tanda “\” yang sekaligus adalah escape characters untuk String di Java. ¬† Dengan demikian, karakter yang berusaha diterjemahkan adalah “\P” (tentunya tidak ada dan salah!). ¬† Bila seandainya tidak ada developer Griffon yang mengalami kesalahan ini, berarti saya satu-satunya yang mencoba men-build Griffon di Windows :p

Salah satu solusi cepat yang saya tempuh untuk memperbaiki situasi di atas adalah dengan menghilangkan ketergantungan ke subproyek griffon-guide (sepertinya ini untuk dokumentasi, sehingga saat ini tidak begitu penting!). Caranya adalah dengan memberikan komentar (atau menghapus) bagian evaluationDependsOn(':griffon-guide') di build.gradle dan include 'griffon-guide' di settings.gradle. Tidak lupa, pada package.gradle, saya juga menghapus bagian from(project(':griffon-guide')) { ... | dan seluruh bagian into('doc') {..} into ('guide') {..}.

Setelah itu, saya kembali menklik icon Run di IntelliJ IDEA. Kali ini, build berjalan dengan lancar, yang ditandai dengan tulisan BUILD SUCCESSFUL.   Agar lebih yakin, saya dapat memeriksa lokasi %GRIFFON_HOME% apakah sudah berisi atau masih kosong.