Menghindari CORS saat memakai Spring Boot bersama dengan Webpack

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat proyek baru yang menggabungkan back end dan front end. Untuk menjalankan back end, saya memberikan perintah ./gradlew bootRun yang akan membuat instance server Tomcat baru. Saya bisa meng-eksekusi end points milik back end di http://localhost:8080/api. Setiap kali kode back end berubah, server ini akan diperbaharui. Sementara itu, untuk menjalankan front end, saya memberikan perintah yarn dev yang juga akan menjalankan server baru di http://localhost:9090. Setiap kali kode front end berubah, server ini akan diperbaharui.

Apa yang salah bila server front end berbeda dengan back end? Hampir semua browser modern sudah mendukung fasilitas Cross-origin Resource Sharing (CORS) yang akan memblokir akses end points milik front end dari domain berbeda. Tentunya saya tidak ingin setiap API milik back end saya bisa diakses oleh front end milik orang lain sesuka hati. Akan tetapi, saat mengembangkan kode program, server front end dan back end adalah dua server berbeda. Fasilitas CORS akan membuat front end saya gagal memanggil back end. Memang Spring Boot memiliki annotation @CrossOrigin yang bisa membuat back end saya dipakai semua orang sesuka hati sehingga front end saya yang berada di server berbeda bisa memanggilnya. Akan tetapi, ini membuat saya merasa kurang aman.

Apakah ada solusi lain? Yup! Saya beruntung karena template webpack yang dipakai oleh vue-cli sudah memperhitungkan hal ini. Bila saya memperhatikan isi file dev-server.js yang dijalankan oleh yarn dev, terlihat bahwa http-proxy-middleware sudah disertakan. Proyek ini pada dasarnya adalah sebuah HTTP proxy (memakai http-proxy) berbasis JavaScript. Dengan http-proxy-middleware, saya ingin pada saat front end mengakses http://localhost:9090/api, secara transparan, ia akan memanggil http://localhost:8080/api. Karena proxy ini transparan dari sisi browser, saya tidak akan menemukan masalah CORS lagi.

Untuk itu, saya akan mengubah file config/index.js pada bagian dev.proxyTable sehingga terlihat seperti berikut ini:

module.exports = {
  ...
  dev: {
    ...
    proxyTable: {
      '/api': 'http://localhost:8080'
    }
  }
}

Pada konfigurasi di atas, setiap kali saya mengakses url yang diawali oleh /api di server front end seperti http://localhost:9090/api/snakes, akses tersebut akan diteruskan ke server back end di http://localhost:8080/api/snakes secara transparan. Dengan demikian, saya tidak akan mendapatkan kesalahan CORS lagi.

Iklan

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.

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.

Apa Bedanya Adobe Illustrator Dengan Photoshop?

Pada suatu hari, saya meminta mahasiswa untuk merancang gambar yang dipakai pada pemograman game. Saya menyarankan mereka untuk menggunakan Adobe Illustrator. Seorang mahasiswa pencinta Photoshop kontan protes dan segera membela tool kesayangannya. Mengapa memakai Illustrator? Berbeda dengan Photoshop, Illustrator menyimpan data dalam bentuk vector sehingga scalable. Bila pengguna Photoshop cenderung menggunakan brush untuk mempoles gambar, maka pengguna Illustrator lebih sering memanipulasi bentuk (shape dan anchor point). Hal ini membuat Illustrator sangat nyaman dipakai untuk merancang gambar seperti kartun dan icon.

Pada Illustrator, terdapat 2 tool pemilihan utama yang sering digunakan, yaitu Selection Tool (V) dan Direct Selection Tool (A) seperti yang terlihat pada gambar berikut ini:

Selection dan Direct Selection Tool

Selection dan Direct Selection Tool

Dengan Direct Selection Tool, saya bisa memilih setiap titik (disebut sebagai anchor point) pada sebuah bentuk. Dengan demikian, saya bisa memanipulasi setiap anchor point yang ada. Sebagai contoh, saya bisa mengubah sebuah persegi panjang menjadi trapesium:

Memodifikasi anchor point

Memodifikasi anchor point

Saya juga bisa melakukan hal di atas dengan Photoshop! Untuk menghapus atau menambah anchor point di Photoshop, saya dapat memilih tool yang terletak di kotak yang sama dengan Pen Tool (P). Selain itu, Direct Selection Tool (A) di Photoshop tersembunyi karena terdapat di kotak yang sama dengan Path Selection Tool (A).

Illustrator memiliki menu khusus untuk memanipulasi path yang dapat diakses di Object, Path. Sebagai contoh, tidak ada cara cepat untuk membuat shape segitiga di versi Photoshop yang saya pakai. Sebaliknya, membuat segitiga sangat mudah di Illustrator. Saya mulai dengan membuat sebuah persegi panjang dan memilih anchor point di sudut kiri atas dan kanan atas. Setelah itu, seusai memakai menu Object, Path, Average…, saya akan memperoleh sebuah segitiga seperti pada gambar berikut ini:

Membuat segitiga

Membuat segitiga

Sama seperti di Photoshop, saya juga dapat mengatur stroke dan mengisi sebuah bentuk dengan gradien di Illustrator. Sebagai contoh, saya akan menghapus stroke dan mengisi bentuk trapesium yang saya buat dengan gradien seperti pada gambar berikut ini:

Mengisi trapesium dengan gradien

Mengisi trapesium dengan gradien

Satu hal yang tidak bisa saya lakukan di Photoshop secara cepat adalah menghaluskan ujung (rounding) pada trapesium ini. Photoshop memang menyediakan pengaturan penghalusan ujung untuk persegi panjang, tapi setelah dimodifikasi menjadi trapesium, saya tidak menemukan pengaturan tersebut lagi. Di Illustrator, saya dapat melakukan hal ini cukup dengan memilih menu Effect, Stylize, Round Corners… seperti pada gambar berikut ini:

Menghaluskan ujung trapesium

Menghaluskan ujung trapesium

Saya kemudian membuat dua trapesium lain di samping kiri dan kanan sehingga gambar saya terlihat seperti pada gambar berikut ini:

Membuat trapesium baru di sisi kiri dan kanan

Membuat trapesium baru di sisi kiri dan kanan

Sekarang, saya ingin sebuah trapesium di bagian bawah untuk melengkapi bagian yang kosong. Pada Photoshop, saya harus menggambarnya secara manual. Illustrator menawarkan cara lain melalui penggunaan Shape Builder Tool (Shift+M). Dengan Shape Builder Tool, saya bisa menggabungkan atau menghapus (dengan menahan tombol Alt) bagian kombinasi dari beberapa shape. Sebagai contoh, saya membuat sebuah persegi panjang dan meletakkannya di belakang (dengan memilih Object, Arrange, Send To Back) seperti pada gambar berikut ini:

Menambahkan sebuah persegi panjang

Menambahkan sebuah persegi panjang

Setelah itu, saya memilih seluruh shape yang ada, kemudian membuang bagian yang tidak dibutuhkan dengan menggunakan Shape Builder Tool:

Memakai shape builder tool

Memakai shape builder tool

Setiap shape di Illustrator bisa memiliki lebih dari satu fill atau stroke dengan effect-nya masing-masing, seperti yang terlihat pada gambar berikut ini:

Lebih dari 1 fill pada shape yang sama

Lebih dari 1 fill pada shape yang sama

Hal serupa dapat dicapai di Photoshop dengan cara yang lebih rumit, misalnya dengan membuat beberapa fill layer yang dikombinasikan dengan layer mask sehingga hanya berlaku untuk shape yang dikehendaki (bukan bagian gambar lainnya).

Langkah berikutnya, saya menambahkan sebuah persegi panjang baru sehingga gambar saya terlihat seperti pada gambar berikut ini:

Membuat persegi panjang baru

Membuat persegi panjang baru

Untuk memberikan ujung yang baru pada bagian bawah persegi panjang, saya dapat menggunakan Direct Selection Tool untuk memilih kedua anchor point di ujung kiri bawah dan kanan bawah, kemudian mengisi nilai corners seperti pada gambar berikut ini:

Menghaluskan hanya ujung kiri bawah dan kanan bawah

Menghaluskan hanya ujung kiri bawah dan kanan bawah

Hal serupa juga dapat dilakukan di Photoshop dengan cara yang berbeda, yaitu dengan mengisi nilai rounding di window Properties.

Bila pengguna Photoshop seringkali memoles gambar dengan menggunakan brush, maka pengguna Illustrator memoles kartun dengan potongan shape melalui fasilitas Pathfinder. Sebagai contoh, saya bisa menduplikasi persegi panjang yang baru dibuat sebanyak dua kali. Saya kemudian menggeser salah satu persegi panjang ke kanan sebanyak 1 piksel. Setelah itu, saya memilih kedua persegi panjang dan memilih Minus Front dari window Pathfinder untuk memperoleh sebuah shape baru seperti pada gambar berikut ini:

Memakai pathfinder

Memakai pathfinder

Saya kemudian melakukan hal yang sama untuk membuat shape di ujung kanan. Setelah itu, saya bisa memberikan gradiasi berbeda pada shape baru yang terbentuk di ujung kiri dan kanan, seperti pada gambar berikut ini:

Gradiasi berbeda di ujung sisi  kiri dan kanan

Gradiasi berbeda di ujung sisi kiri dan kanan

Karena hasil akhir di Illustrator adalah vector, saya tidak perlu khawatir gambar menjadi rusak saat diperbesar atau diperkecil. Vector aman untuk di-resize. Sebagai contoh, pada hasil akhir berikut ini, gambar dapat ditampilkan dengan baik pada beberapa ukuran yang berbeda:

Gambar dengan ukuran 256 x 256 piksel

Gambar dengan ukuran 256 x 256 piksel

Gambar dengan ukuran 128 x 128  piksel

Gambar dengan ukuran 128 x 128 piksel

Gambar dengan ukuran 64 x 64  piksel

Gambar dengan ukuran 64 x 64 piksel

Gambar dengan ukuran 48 x 48 piksel

Gambar dengan ukuran 48 x 48 piksel

Gambar dengan ukuran 32 x 32 piksel

Gambar dengan ukuran 32 x 32 piksel

Melakukan Front-End Testing Dengan Codeception

Pada artikel Melakukan Front-End Testing Dengan Selenium Tanpa “Mengotori” Database, saya mencoba melakukan pengujian halaman web secara otomatis. Pengujian dilakukan dengan mengisi database dengan data yang konsisten lalu mensimulasikan akses pada browser dan memastikan hasilnya sesuai harapan. Pada artikel ini, saya akan melakukan hal yang sama tetapi dengan menggunakan Codeception. Codeception adalah sebuah ‘framework’ pengujian yang mempermudah menulis kode program pengujian. Dukungan yang dimiliki oleh Codeception ditawarkan dalam bentuk module seperti Selenium2, Webdriver, PhpBrowser, dan lainnya.

Sebagai latihan, saya akan menguji sebuah web sederhana dimana terdapat sebuah halaman yang hanya bisa ditampilkan setelah pengguna login. Proyek web tersebut dibuat dengan menggunakan fasilitas Auth yang disediakan oleh Laravel.

Untuk men-install Codeception pada proyek Laravel, saya akan menggunakan perintah Composer berikut ini:

C:\proyek> composer require "codeception/codeception:*"

Composer akan men-download Codeception pada folder vendor/codeception.

Langkah pertama untuk memakai Codeception adalah menghasilkan file konfigurasi dengan memberikan perintah seperti berikut ini:

C:\proyek> vendor\bin\codecept bootstrap app

Perintah di atas akan menyebabkan Codeception membuat beberapa file baru yang dibutuhkan oleh Codeception di folder app\tests. Folder acceptance, functional dan unit masing-masing mewakili jenis pengujian. Istilah yang dipakai untuk front-end testing disini adalah acceptance testing. Ini yang akan saya pakai nantinya.

Sebelumnya, saya perlu mencari file file acceptance.suite.yml dan mengubah nilai url agar sesuai dengan lokasi URL aplikasi yang hendak diuji. Ini adalah salah satu konfigurasi yang dibutuhkan oleh PHPBrowser. Secara default, Codeception akan memakai module PHPBrowser untuk front-end testing. Karena aplikasi yang saya uji tidak mengandung JavaScript, maka ini adalah pilihan yang masuk akal. Bila hendak menguji interaksi melalui JavaScript pada aplikasi, saya harus memakai module Selenium2.

Untuk membuat sebuah file pengujian baru, saya akan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept generate:cept acceptance Login -c app

Perintah di atas akan membuat sebuah file baru bernama LoginCept.php di folder app\tests\acceptance. Saya dapat membuat kode program yang mewakili pengujian di file ini. Sebagai contoh, saya dapat mengubah isinya menjadi seperti berikut ini:

<?php
$I = new WebGuy($scenario);

$I->wantTo('display home page');
$I->amOnPage('home/welcome');
$I->dontSee('Selamat datang');
$I->see('Login');

$I->wantTo('login');
$I->fillField('nama', 'snake');
$I->fillField('password', '12345');
$I->click('Login');
$I->amOnPage('home/welcome');
$I->see('Selamat datang, snake');
$I->seeLink('logout');

Kode program di atas sangat mudah dipahami, bukan? Untuk menjalankannya, saya akan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept run

Saya akan memperoleh hasil seperti berikut ini:

Codeception PHP Testing Framework v1.8.5
Powered by PHPUnit 3.7.37 by Sebastian Bergmann.

Acceptance Tests (1) ------------------------
Trying to login (LoginCept.php)         Ok
---------------------------------------------

Functional Tests (0) ------------------------
---------------------------------------------

Unit Tests (0) ------------------------------
---------------------------------------------


Time: 858 ms, Memory: 6.50Mb

OK (1 test, 4 assertions)

Salah masalah dengan pendekatan skenario seperti di atas adalah saya harus memiliki 1 file PHP per skenario. Sebagai contoh, walaupun pada kode program pengujian, saya memiliki 2 buah wantTo(), hasil pengujian hanya menampilkan yang terakhir kali saja (Trying to login). Untuk mengatasi hal seperti ini, saya dapat mendeklarasi pegujian dalam bentuk class. Untuk itu, saya membuat skenario baru dengan memberikan perintah seperti berikut ini:

C:\proyek> vendor\bin\codecept generate:cest acceptance Auth -c app

Perhatikan bahwa pada perintah di atas, saya memakai generate:cest sementara pada perintah sebelumnya, saya memakai generate:cept. Perintah tersebut akan menghasilkan sebuah file baru bernama AuthCest.php di folder acceptance. File tersebut berisi class AuthenticationCest yang akan saya ubah menjadi seperti berikut ini:

<?php
use WebGuy;

class AuthCest
{

    public function visit(WebGuy $I) {
        $I->wantTo('display home page as guest');
        $I->amOnPage('home/welcome');
        $I->dontSee('Selamat datang');
        $I->see('Login');
    }

    /**     
     * @before login
     */
    public function wrongLogin(WebGuy $I) {
        $I->wantTo('login with wrong password');
        $I->lookForwardTo('see error when entering wrong password');
        $I->amOnPage('login/show');
        $I->fillField('nama', 'snake');
        $I->fillField('password', 'wrong password');
        $I->click('Login');                
        $I->seeInField('Nama', 'snake');
        $I->see('Terjadi kegagalan');      
    }

    public function login(WebGuy $I) {
        $I->wantTo('login');
        $I->amOnPage('login/show');
        $I->fillField('nama', 'snake');
        $I->fillField('password', '12345');
        $I->click('Login');
        $I->amOnPage('home/welcome');
        $I->see('Selamat datang, snake');
        $I->seeLink('logout');
    }

    /**     
     * @after login
     */
    public function logout(WebGuy $I) {
        $I->wantTo('logout');
        $I->seeLink('logout');
        $I->click('logout');
        $I->dontSee('Selamat datang');
        $I->see('Nama');
        $I->see('Password');
        $I->see('Login');
    }

}

Bila saya menjalankan pengujian, kali ini saya akan memperoleh hasil seperti:

Codeception PHP Testing Framework v1.8.5
Powered by PHPUnit 3.7.37 by Sebastian Bergmann.

Acceptance Tests (4) ------------------------------------------------
Trying to display home page as guest (AuthCest.visit)           Ok
Trying to login with wrong password (AuthCest.wrongLogin)       Ok
Trying to login (AuthCest.login)                                Ok
Trying to logout (AuthCest.logout)                              Ok
---------------------------------------------------------------------

Functional Tests (0) ------------------------
---------------------------------------------

Unit Tests (0) ------------------------------
---------------------------------------------


Time: 1.59 seconds, Memory: 6.75Mb

OK (4 tests, 15 assertions)

Selain menjalankan pengujian, saya juga bisa mengubah kode program pengujian menjadi sesuatu yang lebih mudah dibaca (oleh mereka yang bukan developer) dengan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept generate:scenarios acceptance -c app

Hasilnya dapat dilihat pada folder app/tests/_data/scenarios/acceptance. Saya akan menemukan file seperti Auth_Cest.login.txt, Auth_Cest.logout.txt, dan sebagainya. Sebagai contoh, bila saya melihat isi file Auth_Cest.login.txt, saya akan menemukan hasil seperti berikut ini:

I WANT TO LOGIN

I am on page "login/show"
I fill field "nama","snake"
I fill field "password","12345"
I click "Login"
I am on page "home/welcome"
I see "Selamat datang, snake"
I see link "logout"

Menghasilkan SDOCML Berdasarkan Dokumentasi jQuery

Pada artikel di Menambah Content Assistant Untuk jQuery di Aptana Studio 3, saya mencoba untuk mempelajari cara kerja IDE dan fitur content assist pada platform Eclipse (karena Aptana Studio memakai Eclipse Platform). Saya berhasil membuat content assist untuk jQuery yang bekerja sesuai dengan harapan saya (belum tentu sesuai dengan harapan orang lain, karena ini adalah proyek eksperimen 🙂 ). Syaratnya adalah saya harus memiliki sebuah file SDOCML yang mendeskripsikan jQuery pada proyek yang sedang aktif. JavaScript memang tidak memiliki fitur dokumentasi yang standar dan SDOCML merupakan salah satu yang ada di pasaran. jQuery sendiri memiliki dokumentasi dalam bentuk XML yang mirip seperti SDOCML tapi tidak compatible sehingga saya tidak bisa memakainya secara langsung di Aptana Studio. Apa yang dilihat di http://api.jquery.com dihasilkan berdasarkan file XML tersebut.

Seiring waktu berlalu, file SDOCML yang saya buat bisa kadaluarsa karena API jQuery juga terus berevolusi. Untuk mengatasi masalah tersebut, saya membuat sebuah script Groovy yang kode programnya dapat dilihat di https://github.com/JockiHendry/sdocml-generator/blob/master/generate-jquery.groovy. Script ini akan menghasilkan SDOCML berdasarkan dokumentasi jQuery yang ada. Karena dibuat dengan bahasa pemograman Groovy, maka untuk menjalankannya dibutuhkan Java dan Groovy.

Kenapa memakai Groovy? Karena Groovy menyediakan segala sesuatu yang saya butuhkan sehingga saya bisa bekerja secara cepat. Groovy menyediakan CliBuilder sehingga saya hanya perlu mendaftarkan parameter dan Groovy (tepatnya Apache Commons CLI) akan memeriksa apakah pengguna mengisi parameter dengan benar. Groovy juga menyediakan XmlSlurper sehingga saya bisa membaca dokumentasi resmi jQuery dalam bentuk XML secara mudah. Selain itu, Groovy juga memiliki MarkupBuilder sehingga saya bisa menulis file XML dengan mudah (SDOCML pada dasarnya adalah file XML).

Script ini dapat dijalankan dengan menggunakan perintah seperti:

C:\> groovy generate-jquery.groovy -help
error: Missing required option: output
usage: generate-jquery [options]
 -file      retrieve XML API documentation from XML file
 -help            print this message
 -output    output JsDoc to this file
 -url        retrieve XML API documentation from this URL. Default
                  URL is http://api.jquery.com/resources/api.xml

Dokumentasi jQuery dipublikasikan dalam bentuk XML di lokasi http://api.jquery.com/resources/api.xml. Yang akan dilakukan oleh script ini adalah membaca dokumentasi jQuery tersebut lalu menerjemahkan menjadi file SDOCML yang dapat dipakai oleh Aptana Studio. Dengan demikian, saya bisa mendapatkan dokumentasi terbaru dengan mudah.

Bila komputer terkoneksi dengan internet, maka saya bisa menghasilkan file SDOCML berdasarkan dokumentasi jQuery yang terbaru dengan menggunakan perintah seperti:

C:\> groovy generate-jquery.groovy -url -output jquery.sdocml
Reading XML from url: http://api.jquery.com/resources/api.xml
File jquery.sdocml has been generated!

Bila seandainya saya tidak terkoneksi dengan internet, maka saya bisa menghasilkan file SDOCML berdasarkan file XML yang sudah saya download sebelumnya dengan menggunakan perintah seperti:

C:\> groovy generate-jquery.groovy -file jquery-api.xml -output jquery.sdocml
Reading XML from file: jquery-api.xml
File jquery.sdocml has been generated!

Setelah file SDOCML dihasilkan, saya tinggal meletakkannya pada proyek di Aptana Studio dan content assist-pun akan bekerja berdasarkan dokumentasi yang terbaru. Karena file SDOCML ini dihasilkan secara otomatis, bagian example akan terlihat sangat panjang sekali sesuai dengan yang ada di situs http://api.jquery.com.

Memakai Fasilitas Copy CSS Dari Photoshop CC

Sebagai seorang developer, saya hanya sesekali memakai Adobe Photoshop untuk melakukan photo retouching. Tapi bagi web designer, Photoshop adalah tool yang sangat berguna untuk membuat konsep website yang hendak dirancang. Setelah klien setuju dengan rancangan di Photoshop, maka tim front-end bisa segera mengubah gambar statis di Photoshop tersebut menjadi sebuah website yang responsif melalui Adobe Edge Reflow, Adobe Dreamweaver, dan sebagainya. Website yang responsif biasanya dihasilkan dengan menggunakan CSS layout (kebalikannya, website yang tidak responsif hanyalah komposisi gambar yang di-slice dari Photoshop dengan ukuran yang fixed).

Salah satu fitur menarik dari Photoshop yang berguna bagi saya adalah fasilitas Copy CSS. Fasilitas ini memungkinkan Photoshop untuk menghasilkan CSS berdasarkan layer yang sedang saya pilih. Fitur ini berguna bagi saya karena saya tidak mempublikasikan seluruh rancangan ke Adobe Edge Reflow tapi hanya sesekali perlu men-copy CSS berdasarkan apa yang telah dibuat di Photoshop.

Sebagai contoh, saya memiliki dokumen Photoshop dengan 3 layer seperti pada gambar berikut ini:

Dokumen Photoshop Dengan 3 Layer

Dokumen Photoshop Dengan 3 Layer

Saya kemudian memilih layer dengan nama header, lalu memilih menu Layer, Copy CSS seperti pada gambar berikut ini:

Memilih menu Copy CSS

Memilih menu Copy CSS

Photoshop akan men-copy CSS seperti berikut ini pada clipboard:

.header {
  background-image: -moz-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
  background-image: -webkit-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
  background-image: -ms-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
  position: absolute;
  left: 0px;
  top: 0px;
  width: 4608px;
  height: 822px;
  z-index: 2;
}

CSS ini bisa langsung saya pakai pada HTML. Sebagai contoh, untuk membuktikan bahwa CSS tersebut sesuai dengan tampilan di Photoshop, maka saya membuat sebuah file HTML dengan nama latihan.html yang isinya seperti berikut ini:

<html>
<head>
<title>Latihan</title>
<style>
.header {
   background-image: -moz-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
   background-image: -webkit-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
   background-image: -ms-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%); 
   position: absolute;
   left: 0px;
   top: 0px;
   width: 4608px;
   height: 822px;
   z-index: 2;
}
</style>
</head>
<body>
<div class='header'>
</div>
</body>
</html>

Bila saya menampilkan HTML tersebut di browser, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil di browser

Hasil di browser

Hampir sama seperti di Photoshop, bukan? Hanya saja saya perlu melakukan sedikit pengaturan lagi untuk ukuran dan posisi. Misalnya saya bisa menghilang position: absolute dan mengganti ukuran menjadi seperti left: 0px; top: 0px; width: 100%; height: 200px;.

Bagaimana dengan tulisan? Apakah Photoshop CC juga bisa men-copy drop shadow effect dengan baik? Saya akan mengujinya dengan memilih layer dengan nama title, lalu memilih menu Layer, Copy CSS. Saya akan memperoleh CSS seperti berikut ini di clipboard:

.title {
  font-size: 222.5px;
  font-family: "Charlemagne Std";
  color: rgb( 255, 255, 255 );
  font-weight: bold;
  text-align: center;
  text-shadow: 0px 0px 79px rgb( 210, 248, 93 );
  position: absolute;
  left: -36.363px;
  top: 144.887px;
  width: 2350px;
  height: 365px;
  z-index: 3;
}

Saya akan coba menambahkannya di HTML sehingga isi file latihan.html terlihat seperti berikut ini:

<html>
<head>
<title>Latihan</title>
<style>
.header {
   background-image: -moz-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
   background-image: -webkit-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
   background-image: -ms-linear-gradient( 90deg, rgb(255,255,255) 0%, rgb(0,0,0) 18%);
   left: 0px;
   top: 0px;
   height: 200px;
}
p.title {
   font-size: 3em;
   font-family: "Charlemagne Std";
   color: rgb( 255, 255, 255 );
   font-weight: bold;
   text-align: center;
   text-shadow: 0px 0px 15px rgb( 210, 248, 93 );
   margin-top: 0px; padding-top: 40px;
}
</style>
</head>
<body>
<div class='header'>
   <p class='title'>The Solid Snake</p>
</div>
</body>
</html>

Saya melakukan sedikit perubahan pada CSS yang dihasilkan oleh Photoshop untuk menyesuaikan dengan posisi yang ada di HTML. Bila saya menampilkan latihan.html di browser, kali ini saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil di browser

Hasil di browser

Hampir mirip dengan yang ada di Photoshop. Hanya saja saya perlu menyesuaikan posisi dan ukuran sesuai dengan yang dibutuhkan di HTML. Misalnya saya memilih memakai 3em daripada 222.5px karena ukuran dalam satuan px bisa sangat berbeda dari satu platform ke platform lainnya.

Aptana Journal #12: Selesai Tanpa Kiamat

Tanggal 21 Desember 2012 dikabarkan adalah akhir dari peradaban manusia.   Beruntungnya, tanggal 21 sudah berlalu, dan sekarang sudah memasuki tanggal 22 (at least, bila  jam kiamat berdasarkan patokan waktu di Indonesia).   Hari ini memang adalah hari yang melelahkan bagi saya, tapi tidak ada tanda-tanda kiamat selain hujan yang turun terus menerus.  Okay, ini berarti saya masih punya kesempatan untuk membuat post blog baru.

Beberapa waktu lalu, saya membaca panduan kurikulum ilmu komputer yang dirancang oleh ACM (Association for Computing Machinary).   Apa itu ACM?  ACM adalah organisasi yang bergerak di bidang pendidikan dan penelitian advance computing dimana anggotanya adalah para praktisi di bidangnya.  ACM memiliki panduan kurikulum yang kerap dijadikan sebagai masukan oleh berbagai universitas di dunia.

Kurikulum ACM merefleksikan apa yang dibutuhkan oleh industri saat ini.  ACM juga terus merevisi kurikulumnya dengan meminta masukan dari para praktisi di industri.  Ini adalah hal bagus, karena selama ini, saya sering merasa kalangan akademis menciptakan sebuah “dunia semu” bagi mahasiswanya.  Mahasiswa dipacu untuk mengerjakan ujian yang merefleksikan “dunia semu” ini.  Lalu begitu lulus, mahasiswa harus beradaptasi lagi dengan “dunia nyata” (industri!) yang ternyata beda jauh dari “dunia semu” di kampus.  Bukankah sungguh ironis: mahasiswa mati-matian berjuang mendapat nilai ‘A’ hanya untuk kualitas “dunia semu”;  para mafia kampus akan meningkatkan tingkat kesulitan “dunia semu” menjadi “dunia tak jelas” sehingga mereka mendapat ‘uang saku’ tambahan; dan mahasiswa akan berjuang merayu dosen agar tidak pelit nilai (bila sudah kerja nanti, apakah mereka akan merayu bos agar naik gaji?)  Bila ini dibiarkan terus menerus, kampus akan membentuk sebuah ‘sistem’ tersendiri yang semakin terpisah dari dunia industri.  Gelar akademis dan level golongan dosen lama-lama akan menjadi formalitas tanpa isi, tanpa kebanggaan (mungkin yang tersisa hanya ‘egoisme’ anak kecil dalam membela almamater).

Pada panduan yang saya baca, salah satu masukan dari industri adalah untuk meningkatkan pelajaran yang berkaitan dengan keamanan komputer dan software archeology.  Arkeologi?  Ini bukan pelajaran sejarah ‘kan?!  Software archeology adalah sebuah aktifitas untuk mempelajari kode program buatan orang lain dimana seseorang tidak memiliki dokumentasi yang lengkap atas kode program tersebut.  Ibaratkan dengan arkeologi, kode program buatan orang lain tersebut adalah sebuah artifact bersejarah.  Seorang arkeolog akan memprediksi kenapa ada tumpukan batu di dekat kuil kuno, ia akan menganalisa benda-benda sekitarnya untuk menemukan jawaban.  Dalam software archeology,  seseorang juga harus menganalisa kenapa sebuah bagian kode program dibuat dan untuk tujuan apa.

Mengapa software archeology dianggap penting oleh industri?  Karena, pada saat seorang lulusan bekerja di industri, mereka tidak selalu membuat software dari awal.  Kebanyakan yang terjadi adalah mereka harus bergabung dengan tim dimana proyek mungkin sudah berjalan 1 atau 2 bulan.   Kadang-kadang mereka adalah pengganti karyawan lama yang tiba-tiba menghilang sehingga mereka harus bekerja tanpa sempat melalui transfer ilmu.   Suatu hari nanti, mungkin mereka harus merombak sebuah sistem lama yang sudah dibuat 5 tahun lalu dimana seluruh developer-nya sudah resign.  Disinilah software archeology mulai menunjukkan perannya.

Apa yang saya lakukan selama seri artikel ini, mulai dari Aptana Journal #1 hingga Aptana Journal #12, adalah upaya memahami dan mengubah kode program untuk sebuah software yang dibuat orang lain, yaitu Aptana Studio 3.  Saya tidak punya dokumentasi yang lengkap.  Di kode program Aptana Studio 3, tidak selalu ada JavaDoc!    Ini adalah software archeology.  Tidak ada pembuat kode program original untuk ditanyai (bukan karena orang-orang tersebut adalah manusia purba yang sudah punah!)  Tentu saja, software archeology yang serius memiliki metode formal dan terstruktur, dan saya hanya memakai metode menulis dokumentasi dalam HTML (juga ada class diagram dan sequence diagram yang menempel di dinding!).

Mengubah Aptana Studio 3 untuk mendukung jQuery melalui polymorphism ‘semu’ mungkin tidak selalu berguna bagi semua orang, tapi hal ini akan sangat membantu saya.  Salah satu bagian yang cukup melelahkan adalah mendokumentasikan setiap method dan property jQuery yang ada di http://api.jquery.com ke dalam file ScriptDoc XML.   File yang saya buat tersebut dapat ditemukan di https://docs.google.com/open?id=0B-_rVDnaVRCbNEw5OU1WRUdXM1U.

Perilaku content assist dan context info yang telah berubah dapat dilihat pada animasi berikut (ini adalah animasi GIF berukuran 2.5 MB):

Animasi Yang Menunjukkan Hasil Perubahan

Aptana Journal #11: Menambahkan Dukungan Inline JSDoc

Artikel sebelumnya adalah salah satu post yang menarik di akhir tahun 2012 ini karena artikel tersebut adalah artikel ke-212 yang saya tulis di blog ini 🙂

Post ke 212 Untuk Seri ke 10 Di Akhir Tahun 2012

Post ke 212 Untuk Seri ke 10 Di Akhir Tahun 2012

Dan kembali ke artikel 213,  saya dihadapkan permasalahan bagaimana menampilkan content assist untuk argumen dalam anonymous inner function, misalnya yang paling sering ditemui adalah jQuery.Event.  Sebagai contoh, content assist tidak bekerja dengan baik di gambar berikut ini:

Content Assist Tidak Menampilkan proposal untuk object jQuery.Event

Content Assist Tidak Menampilkan proposal untuk object jQuery.Event

Padahal, e adalah sebuah variabel dengan tipe jQuery.Event. Dan saya sudah menambahkan bagian berikut ini ke ScriptDoc XML yang dipakai:

<javascript>
   ... <!-- isi diabaikan -->

   <class type="jQuery.Event">
     <properties>
        <property name="currentTarget" type="Element" scope="instance">
           <description>The current DOM element within the event bubbling phase.</description>
        </property>
     </properties>
   </class>
</javascript>

Dalam bahasa seperti JavaScript, memang sangat sulit untuk menentukan apa tipe dari sebuah variabel.  Tapi, bila programmer JavaScript menambahkan sebuah komentar dengan makna khusus, misalnya JSDoc, maka Aptana Studio bisa mengetahui tipe dari variabel e dan menampilkan content assist untuk variabel tersebut.  Saat ini, Aptana Studio mendukung penggunaan @type untuk elemen selain function sehingga content assist bekerja dengan baik bila saya menambahkan JSDOC tersebut seperti berikut ini:

Menggunakan JSDoc @type di Aptana Studio

Menggunakan JSDoc @type di Aptana Studio

Tapi saya merasa syntax tersebut masih terlalu panjang.  Oleh sebab itu, saya mencoba memodifikasi Aptana Studio agar mendukung inline JSDoc yang lebih singkat.

Hasil penelusuran membawa saya ke class JSTypeUtil di method applyDocumentation(). Method ini akan memeriksa apakah property adalah FunctionElement. Bila iya, maka ia akan memberikan dokumentasi untuk FunctionElement tersebut.  Jika bukan, ia memberikan dokumentasi ke PropertyElement yang ada berdasarkan block.getText(), block.getTags(TagType.TYPE), dan block.getTags(TagType.EXAMPLE) (dimana block adalah sebuah DocumentationBlock).

Untuk mendukung inline JSDOC, saya perlu mengubah alur di atas,  sehingga kode program akan terlihat seperti berikut ini:

public static void applyDocumentation(PropertyElement property, JSNode node, DocumentationBlock block)
{
   if (property instanceof FunctionElement)
   {
       ... // kode program diabaikan
   }
   else
   {
       if (block != null)
       {
           // Bila tidak ada tag TYPE, maka ini adalah sebuah inline JSDOC
           if (block.getTags(TagType.TYPE).isEmpty()) {
              if (!StringUtil.isEmpty(block.getText().trim())) {
                 property.addType(block.getText().trim());
              }
           } else {

              ... // ini adalah kode program semula
           }
       }
   }
}

Sekarang, bila saya memakai inline JSDoc, maka content assist akan bekerja sesuai dengan yang diharapkan seperti pada gambar berikut ini:

Content Assist Untuk Inline JSDoc

Content Assist Untuk Inline JSDoc

Ops!  Tunggu dulu..  Bila saya memperhatikan contoh inline JSDoc di halaman http://code.google.com/p/jsdoc-toolkit/wiki/InlineDocs, terlihat bahwa tidak ada spasi di antara tipe dan komentar.  Versi perubahan saya saat ini masih tidak compatible.

Oleh sebab itu, saya masih perlu melakukan perubahan.  Saya harus mengubah SDoc.flex  dan SDoc.grammar agar mengenali inline JSDoc yang tidak dipisahkan dengan spasi.

Saya mulai dengan menambahkan sebuah definisi terminal baru di /com.aptana.js.core/parsing/SDoc.flex dengan nama INLINE_DOCUMENTATION seperti berikut ini:

%terminals INLINE_DOCUMENTATION;

Lalu, pada definisi rule Block, saya mengubahnya sehingga terlihat seperti berikut ini:

Block
	=	START_DOCUMENTATION Text.text END_DOCUMENTATION
		{:
			return new DocumentationBlock((String) text.value);
		:}
	|	START_DOCUMENTATION Tags.tags END_DOCUMENTATION
		{:
			return new DocumentationBlock((List<Tag>) tags.value);
		:}
	|	START_DOCUMENTATION Text.text Tags.tags END_DOCUMENTATION
		{:
			return new DocumentationBlock((String) text.value, (List<Tag>) tags.value);
		:}
	|	INLINE_DOCUMENTATION.text
	    {:
	    	return new InlineDocumentationBlock((String)text.value);
	    :}
	;

Pada kode program di atas, saya memisahkan antara dokumentasi biasa yang diwakili oleh class DocumentationBlock dan dokumentasi inline yang diwakili oleh class InlineDocumentationBlock.   Saat ini class InlineDocumentationBlock belum ada, sehingga saya perlu membuatnya.   Karena InlineDocumentationBlock pada dasarnya adalah bentuk khusus dari DocumentationBlock, maka saya tinggal menurunkan class tersebut dari DocumentationBlock.   Class baru ini dibuat di package com.aptana.js.internal.core.parsing.sdoc.model di proyek com.aptana.js.core dengan isi seperti berikut ini:

package com.aptana.js.internal.core.parsing.sdoc.model;

/**
 * Model to represent inline doc comments. See 
 * <a href="http://code.google.com/p/jsdoc-toolkit/wiki/InlineDocs">
 * http://code.google.com/p/jsdoc-toolkit/wiki/InlineDocs</a> for example.
 * 
 * @author SolidSnake
 *
 */
public class InlineDocumentationBlock extends DocumentationBlock {

	public InlineDocumentationBlock(String content) {
		super(content);
	}

}

Setelah itu, pada file /com.aptana.js.core/com/aptana/js/internal/core/parsing/sdoc/SDocTokenType.java, di bagian enumeration SDocTokenType, saya menambahkan baris berikut ini:

  INLINE_DOCUMENTATION(Terminals.INLINE_DOCUMENTATION),

Lalu, saya melakukan perubahan di file , dengan menambahkan baris berikut ini di <YYINITIAL>:

  // inline documentation
  "/**" [^ \t\r\n{\[\]#]+ "*/"  {
	String text = yytext(); 
	return newToken(SDocTokenType.INLINE_DOCUMENTATION, text.substring(3,text.length()-2)); }

Perubahan pada scanner generator dan parser generator telah selesai, saya pun mencoba menjalankan build.js.xml untuk memastikan bahwa file Java bisa dihasilkan dengan baik.  Caranya adalah dengan men-klik kanan file build.js.xml, kemudian memilih Run As, Ant Build.  Setelah memastikan bahwa Console menampilkan tulisan BUILD SUCCESSFUL,  saya mencoba me-refresh package com.aptana.js.internal.core.parsing.sdoc dengan men-klik kanan package tersebut dan memilih Refresh.

Perubahan terakhir yang perlu saya lakukan kembali lagi ke class JSTypeUtil di method applyDocumentation(). Saya kembali mengubah method tersebut sehingga terlihat seperti berikut ini:

public static void applyDocumentation(PropertyElement property, JSNode node, DocumentationBlock block)
{
  if (property instanceof FunctionElement)
  {
    applyDocumentation((FunctionElement) property, node, block);
  }
  else
  {
    if (block != null)
    {
      if (block instanceof InlineDocumentationBlock) {

        if (!StringUtil.isEmpty(block.getText().trim())) {
          property.addType(block.getText().trim());
        }

      } else {

        // apply description
        property.setDescription(block.getText());

        // apply types
        for (Tag tag : block.getTags(TagType.TYPE))
        {
          TypeTag typeTag = (TypeTag) tag;

          for (Type type : typeTag.getTypes())
          {
            ReturnTypeElement returnType = new ReturnTypeElement();

            returnType.setType(type.toSource());
            returnType.setDescription(typeTag.getText());
            property.addType(returnType);
          }
        }

        // apply examples
        for (Tag tag : block.getTags(TagType.EXAMPLE))
        {
          ExampleTag exampleTag = (ExampleTag) tag;

          property.addExample(exampleTag.getText());
        }

      }
    }
  }
}

Sekarang, inline JSDoc bisa bekerja dengan baik seperti yang terlihat pada gambar berikut ini:

Inline JSDoc yang bekerja dengan baik

Inline JSDoc yang bekerja dengan baik

Aptana Journal #10: Menampilkan Parameter Di Content Assist

Walaupun berhasil menampilkan ‘polymorphism‘ di content assist, seluruh nama method yang muncul selalu dengan nama yang sama.  Hal ini terkadang bisa sangat membingungkan.  Oleh sebab itu, saya ingin melakukan perubahan dimana nama method yang muncul di content assist juga menyertakan parameter.

Untuk itu, saya perlu mengubah method addProposal(Set<ICompletionProposal>, PropertyElement, int, URI, String, String[] di class JSContentAssistProcessor dengan menambahkan bagian seperti berikut ini:

PropertyElementProposal proposal = null;
if (property instanceof FunctionElement) {
  FunctionElement function = (FunctionElement) property;
  String documentation = JSModelFormatter.CONTEXT_INFO.getDocumentation(function);
  ContextInformation contextInformation = new ContextInformation(function.getName(), documentation);

  String proposalValue = StringUtil.join(null, function.getName(), "(",
    StringUtil.join(", ", function.getParameterNames()), ")");
  proposal = new PropertyElementProposal(proposalvalue, function.getName()+"()", function.getName().length()+1,
    function, offset, replaceLength, projectURI, contextInformation);
}

Langkah berikutnya adalah mengubah constructor yang pernah saya tambahkan di PropertyElementProposal.  Saya akui bahwa perubahan ini bukan yang terbaik, karena saya hanya menambah tanpa berani mengubah karena takut merusak yang sudah ada.  Akibatnya, parameter constructor terlihat panjang seperti berikut ini:

public PropertyElementProposal(String displayString, String replacementString, int cursorPosition,
  PropertyElement property, int offset, int replaceLength, URI uri, ContextInformation contextInformation)
{
  super(replacementString, offset, replaceLength, cursorPosition, null, displayString, 
    contextInformation, null);
  this.property = property;
  this.uri = uri;
}

Perjuangan belum selesai sampai disini, karena urutan tampilnya content assist tiba-tiba jadi berantakan.  Saya perlu mencari tahu kenapa, dan akhirnya menemukan jawaban di method validate() milik CommonCompletionProposal. Sebagai informasi, class PropertyElementProposal adalah turunan dari CommonCompletionProposal, sehingga method validate() ini ikut dipanggil. Berikut adalah isi method validate():

public boolean validate(IDocument document, int offset, DocumentEvent event)
{
  if (offset < this._replacementOffset)
    return false;

  int overlapIndex = getDisplayString().length - _replacementString.length();
  overlapIndex = Math.max(0, overlapIndex);
  String endPortion = getDisplayString().substring(overlapIndex);
  boolean validated = isValidPrefix(getPrefix(document, offset), endPortion);

  if (validated && event!=null)
  {
     // ... kode program diabaikan
  }

  return validated;
}

Karena nilai _displayString sangat berbeda dengan nilai _replacementString, maka hasil variabel endPortion jadi aneh.  Tapi karena kode program ini bukan saya yang buat, saya tidak mengerti apa tujuan awalnya.

Lalu apa saya harus mengubah method validate() di CommonCompletionProposal?   Tidak, lebih baik saya men-override method validate() ini di PropertyElementProposal sehingga perubahan tidak berdampak CommonCompletionProposal yang lain.   Oleh sebab itu, saya menambahkan method berikut ini di PropertyElementProposal (yang isinya hampir sama seperti di CommonCompletionProposal:

@Override
public boolean validate(IDocument document, int offset, DocumentEvent event)
{
	if (offset < this._replacementOffset)
		return false;

	int posisiKurung = getDisplayString().indexOf('(');
	String propertyName = null;
	if (posisiKurung == -1) {
		propertyName = getDisplayString();
	} else {
		propertyName = getDisplayString().substring(0, getDisplayString().indexOf('('));
	}
	int overlapIndex = propertyName.length() - _replacementString.length();
	overlapIndex = Math.max(0, overlapIndex);
	String endPortion = getDisplayString().substring(overlapIndex);
	boolean validated = isValidPrefix(getPrefix(document, offset), endPortion);

	if (validated && event != null)
	{
		// make sure that we change the replacement length as the document content changes
		int delta = ((event.fText == null) ? 0 : event.fText.length()) - event.fLength;
		final int newLength = Math.max(_replacementLength + delta, 0);
		_replacementLength = newLength;
	}

	return validated;
}

Sekarang, content assist untuk method yang memiliki banyak variasi tidak akan terlihat membingungkan lagi karena sudah ada informasi parameter seperti yang terlihat pada gambar berikut ini:

Content Assist Dengan Informasi Parameter

Content Assist Dengan Informasi Parameter