Memahami Correlation Di Windows Workflow Foundation (WF)

Pada saat memakai WF untuk menyediakan layanan web service, correlation adalah sesuatu yang kerap dipakai. Correlation berkaitan dengan activity Receive dimana sebuah activity Receive akan diterjemahkan menjadi sebuah web service yang dapat dipanggil oleh client. Sebagai contoh, saya akan membuat sebuah proyek baru di Visual Studio 2010 dengan template Visual C# WCF Workflow Service Application.

Saya mulai dengan menghapus template workflow yang sudah ada dan membuat workflow sederhana baru untuk proses registrasi yang berisi activity ReceiveAndSendReply, seperti yang terlihat pada gambar berikut ini:

Membuat sebuah activity ReceiveAndSendReply

Membuat sebuah activity ReceiveAndSendReply

Saya membutuhkan beberapa variabel untuk menampung informasi registrasi, seperti yang terlihat pada gambar berikut ini:

Daftar variabel dalam workflow

Daftar variabel dalam workflow

Untuk menentukan parameter yang dibutuhkan pada saat service Daftar() dipanggil oleh client, saya men-klik tulisan View message… pada bagian Content dan mengisi dialog yang muncul agar terlihat seperti berikut ini:

Parameter untuk activity Receive Daftar()

Parameter untuk activity Receive Daftar()

Saya juga melakukan langkah yang sama untuk menentukan hasil kembalian dari service ini, dengan mengisi dialog yang muncul seperti berikut ini:

Nilai yang dikembalikan oleh service Daftar()

Nilai yang dikembalikan oleh service Daftar()

Selanjutnya, saya menambahkan sebuah activity ReceiveAndSendReply lagi dengan nama operasi Aktivasi. Saya mengisi parameternya seperti pada gambar berikut ini:

Parameter untuk activity Receive Aktivasi()

Parameter untuk activity Receive Aktivasi()

Saya mengisi content definition untuk hasil kembalian dari servce ini seperti yang terlihat pada gambar berikut ini:

Nilai yang dikembalikan oleh service Aktivasi()

Nilai yang dikembalikan oleh service Aktivasi()

Pada workflow sederhana ini, pengguna pertama kali memanggil service Daftar() untuk melakukan proses registrasi. Anggap saja ia kemudian menerima email beserta kode aktivasi yang akan memanggil service Aktivasi(). Workflow-nya akan terlihat seperti pada gambar berikut ini:

Hasil rancangan workflow

Hasil rancangan workflow

Bila saya menjalankan aplikasi ini, WcfClientTester.exe otomatis akan dijalankan dengan tampilan seperti yang terlihat pada gambar berikut ini:

Workflow services yang tersedia

Workflow services yang tersedia

Bila saya mencoba memanggil salah satu dari service Daftar() atau Aktivasi(), saya akan memperoleh pesan kesalahan seperti berikut ini:

There is no context attached to the incoming message for the service and the current operation is not marked with “CanCreateInstance = true”. In order to communicate with this service check whether the incoming binding supports the context protocol and has a valid context initialized.

Mengapa demikian? Apa itu workflow instance? Sama seperti satu atau lebih object adalah instance dari sebuah class, bisa terdapat satu atau lebih instance dari sebuah workflow. Sebagai contoh, proses registrasi untuk email solidsnake@gmail.com adalah instance yang berbeda dari proses registrasi untuk email liquidsnake@gmail.com, walaupun mereka berdasarkan workflow yang sama.

Bila instance dari object akan dibuat dengan keyword new, lalu kapan instance dari workflow akan dibuat? Jawabannya adalah pada saat sebuah activity dengan nilai property CanCreateInstance=true dipanggil. Biasanya ini adalah activity Receive yang didefinisikan pertama kali. Oleh sebab itu, saya mengatur nilai property CanCreateInstance menjadi true untuk activity tersebut seperti yang terlihat pada gambar berikut ini:

Mengatur nilai property CanCreateInstance

Mengatur nilai property CanCreateInstance

Sekarang, saya dapat memanggil operasi Daftar() dengan baik seperti yang terlihat pada gambar berikut ini:

Memanggil service Daftar()

Memanggil service Daftar()

Lalu bagaimana dengan operasi Aktivasi()? Pesan kesalahan tetap akan muncul. Tapi permasalahannya adalah bila saya memberi nilai CanCreateInstance=true pada activity tersebut, maka ia akan membuat instance workflow baru sama halnya seperti Daftar(). Akibatnya, Aktivasi() dapat dipanggil sebelum Daftar(). Padahal, harusnya Aktivasi() adalah bagian dari instance yang diciptakan oleh Daftar(). Permasalahan ini mirip seperti permasalahan session di aplikasi web. Apa yang harus saya lakukan agar operasi dapat dipanggil secara berurut?

Agar bisa mengenali setiap instance, WF memakai correlation untuk memetakan setiap request ke instance yang bersangkutan. Pada contoh ini, saya akan menggunakan content-based correlation dimana saya perlu mencari sebuah nilai unik yang menjadi pengenal instance workflow. Pada workflow proses registrasi yang saya buat, email adalah kandidat yang tepat untuk dipakai sebagai identitas sebuah instance workflow karena ia selalu unik untuk setiap instance.

Saya perlu melakukan inisialisasi nilai correlation dipakai sebagai pengenal workflow instance dengan mengisi nilai property CorrelationInitializers di activity Receive untuk operasi Daftar() dengan nilai seperti berikut ini:

Melakukan inisialisasi pengenal workflow instance

Melakukan inisialisasi pengenal workflow instance

Berikutnya, saya perlu mengatur correlation untuk activity Receive untuk service Aktivasi(). Caranya adalah dengan men-klik tombol di sisi kanan property CorrelatesOn dan mengisi dialog yang muncul dengan informasi seperti pada gambar berikut ini:

Mengatur nilai correlation

Mengatur nilai correlation

Sekarang, bila saya mengerjakan operasi Daftar() sebelum Aktivasi() dengan email yang sama, maka operasi Aktivasi() akan dikerjakan dengan baik, seperti yang terlihat pada gambar berikut ini:

Memanggil service Aktivasi()

Memanggil service Aktivasi()

Tapi bila saya memanggil Aktivasi() dengan sebuah email berbeda yang belum didaftarkan oleh Daftar(), maka saya akan memperoleh pesan kesalahan seperti berikut ini:

The execution of an InstancePersistenceCommand was interrupted because the instance key ‘6ec2e27e-9585-0253-0eb0-707d3d08dfbe’ was not associated to an instance. This can occur because the instance or key has been cleaned up, or because the key is invalid. The key may be invalid if the message it was generated from was sent at the wrong time or contained incorrect correlation data.

Iklan

Memakai WF Bersama Dengan WCF: Workflow Services

Pada artikel sebelumnya, saya menggunakan class WorkflowApplication untuk menjalankan workflow. Sebagai alternatif lain, WF juga menyediakan class WorkflowServiceHost yang memakai WCF. Dengan menggunakan class tersebut, saya dapat mengimplementasikan message exchange pattern (MEP) seperti Datagram (komunikasi satu arah dari client ke server), Request-Response (client menghubungi server dan server menjawab client), dan Duplex (komunikasi dua arah). Caranya adalah dengan menggunakan activity Send, SendReply, Receive atau ReceiveReply saat merancang workflow.

Sebagai latihan, saya akan mengubah workflow restoran yang saya buat sebelumnya menjadi aplikasi n-tier (multi-tier) yang memakai WCF. Presentation tier terdiri atas waitress.exe, kasir.exe, dan koki.exe. Ketiganya dibuat dengan WPF. Pada kasus yang lebih realistis, mungkin saja mereka bisa berupa aplikasi web untuk pemesanan dan aplikasi yang dijalankan di perangkat khusus (embedded). Saya akan membuat 2 business logic tier yaitu sebuah services tier yang memakai workflow dan sebuah data access tier untuk operasi database. Data tier-nya sendiri adalah sebuah database Microsoft SQL Server 2008. Sesungguhnya konfigurasi seperti ini terlalu rumit (overkill) untuk sebuah permasalahan yang sederhana, tetapi setidaknya saya dapat memperoleh bayangan seperti apa yang disebut arsitektur n-tier (multi-tier).

Dengan demikian, hasil akhir yang saya peroleh adalah sebuah solution di Visual Studio yang terdiri atas 5 proyek dimana terdapat 3 proyek WPF (presentation tier), 1 proyek WCF (business logic tier untuk services), dan 1 proyek WF (business logic tier untuk data access).

Membuat Domain Model

Kode program pada proyek business logic tier dan services tier akan memakai domain model yang sama. Boleh dibilang domain model adalah ‘inti sari’ dari permasalahan bisnis sehingga ia bisa saja dibutuhkan diberbagai tempat di bussiness logic tier. Oleh sebab itu, saya akan membuat sebuah proyek baru yang khusus untuk menampung domain class dan persistence layer-nya sehingga mereka bisa dipakai ulang di proyek lainnya.

Saya men-klik kanan nama solution dan memilih menu Add, New Project…. Pada dialog yang muncul, saya memilih Class Library dan mengisi nama proyek dengan Common. Data akan disimpan ke dalam database SQL Server dengan menggunakan Entity Framework 6. Oleh sebab itu, saya memilih Project, Manage NuGet Packages… untuk men-install Entity Framework 6 pada proyek ini.

Berikutnya, saya akan membuat domain class yang isinya terlihat seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations.Schema;

namespace Common
{
    public class Order
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        public DateTime Tanggal { get; set; }

        public String NamaPelanggan { get; set; }

        public decimal Harga { get; set; }

        public Guid WorkflowId { get; set; }

        public virtual List<RiwayatOrder> ListRiwayatOrder { get; set; }

        public String StatusTerakhir { get; set; }

        public void TambahRiwayat(RiwayatOrder riwayat)
        {
            ListRiwayatOrder.Add(riwayat);
            StatusTerakhir = riwayat.Status;
        }
    }

    public abstract class RiwayatOrder
    {

        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        public DateTime Tanggal { get; set; }

        public String Nama { get; set; }

        public abstract String Status { get; }        
    }

    public class RiwayatOrderMulaiDimasak : RiwayatOrder
    {
        public override string Status
        {
            get { return "MulaiDimasak"; }
        }
    }

    public class RiwayatOrderSelesaiDimasak : RiwayatOrder
    {
        public override string Status
        {
            get { return "SelesaiDimasak"; }
        }
    }

    public class RiwayatOrderPelangganMembayar : RiwayatOrder
    {
        public override string Status
        {
            get { return "PelangganMembayar"; }
        }

        public String JenisPembayaran { get; set; }
    }
}

Saya juga akan membuat sebuah persistence context yang isinya seperti berikut ini:

using System.Data.Entity;

namespace Common
{
    public class LatihanWorkflowContext : DbContext
    {
        public LatihanWorkflowContext() : base("LatihanWorkflow") { }

        public DbSet<Order> DaftarOrder { get; set; }

        public DbSet<RiwayatOrder> RiwayatOrder { get; set; }
    }    
}

Model di atas akan menghasilkan tabel di Microsoft SQL Server seperti yang terlihat pada gambar berikut ini:

Struktur tabel yang dihasilkan dari domain model

Struktur tabel yang dihasilkan dari domain model

Saya juga akan menambahkan class untuk data transfer object (DTO) pada proyek ini. Untuk itu, saya menambahkan referensi System.Runtime.Serialization pada proyek ini. Setelah itu, saya membuat sebuah class baru yang isinya terlihat seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace Common
{

    [DataContract]
    public class OrderSimpan
    {
        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String NamaPelanggan { get; set; }

        [DataMember]
        public decimal Harga { get; set; }

        [DataMember]
        public Guid WorkflowId { get; set; }
    }

    [DataContract]
    public class OrderRequest
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String NamaPelanggan { get; set; }

        [DataMember]
        public decimal Harga { get; set; }

        [DataMember]
        public Guid WorkflowId { get; set; }

        [DataMember]
        public List<RiwayatOrderRequest> ListRiwayatOrder { get; set; }

        [DataMember]
        public String StatusTerakhir { get; set; }
    }

    [DataContract]
    public class RiwayatOrderSimpan
    {
        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String Nama { get; set; }

        [DataMember]
        public String Status { get; set; }

        [DataMember]
        public String JenisPembayaran { get; set; }
    }

    [DataContract]
    public class RiwayatOrderRequest
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String Nama { get; set; }

        [DataMember]
        public String Status { get; set; }

        [DataMember]
        public String JenisPembayaran { get; set; }
    }
}

Membuat Data Access Services

Saya akan membuat sebuah proyek baru yang mewakili data access services dengan template Visual C# Console Application yang saya beri nama DataServices. Karena saya akan memakai WCF, maka saya perlu menambahkan referensi System.ServiceModel pada proyek dengan men-klik kanan nama proyek dan memilih Add Reference…. Selain itu, saya juga perlu menambahkan referensi ke proyek Common melalui tab Projects di dialog Add Reference.

Saya perlu mempublikasikan layanan web services untuk melakukan operasi database dengan menggunakan WCF. Oleh sebab itu, saya membuat sebuah class baru dengan nama Services yang isinya seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
using Common;

namespace DataServices
{
    [ServiceContract]
    public class Services
    {

        [OperationContract]
        public int BuatOrder(OrderSimpan orderSimpan)
        {
            Order order = new Order()
            {
                Tanggal = orderSimpan.Tanggal,
                NamaPelanggan = orderSimpan.NamaPelanggan,
                Harga = orderSimpan.Harga,
                WorkflowId = orderSimpan.WorkflowId
            };
            order.StatusTerakhir = "Dipesan";
            using (var db = new LatihanWorkflowContext())
            {
                db.DaftarOrder.Add(order);
                db.SaveChanges();
            }
            return order.Id;
        }

        [OperationContract]
        public Boolean TambahRiwayatOrder(int orderId, RiwayatOrderSimpan riwayatOrderSimpan)
        {
            using (var db = new LatihanWorkflowContext())
            {
                var hasilQuery = from o in db.DaftarOrder
                              where o.Id == orderId
                              select o;
                Order order = hasilQuery.FirstOrDefault();
                if (order != null)
                {
                    RiwayatOrder riwayatOrder = null;
                    if (riwayatOrderSimpan.Status.Equals("MulaiDimasak"))
                    {
                        riwayatOrder = new RiwayatOrderMulaiDimasak();
                    }
                    else if (riwayatOrderSimpan.Status.Equals("SelesaiDimasak"))
                    {
                        riwayatOrder = new RiwayatOrderSelesaiDimasak();
                    }
                    else if (riwayatOrderSimpan.Status.Equals("PelangganMembayar"))
                    {
                        riwayatOrder = new RiwayatOrderPelangganMembayar();
                        (riwayatOrder as RiwayatOrderPelangganMembayar).JenisPembayaran =
                            riwayatOrderSimpan.JenisPembayaran;
                    }
                    else
                    {
                        return false;
                    }

                    riwayatOrder.Nama = riwayatOrderSimpan.Nama;
                    riwayatOrder.Tanggal = riwayatOrderSimpan.Tanggal;

                    order.TambahRiwayat(riwayatOrder);
                    db.SaveChanges();

                    return true;
                }
                return false;
            }
        }

        [OperationContract]
        public List<OrderRequest> LihatSemuaOrder(String statusTerakhir)
        {
            var result = new List<OrderRequest>();
            using (var db = new LatihanWorkflowContext())
            {
                var hasilQuery = from o in db.DaftarOrder
                                 where o.StatusTerakhir == statusTerakhir
                                 select o;

                foreach (Order order in hasilQuery)
                {
                    OrderRequest orderRequest = new OrderRequest()
                    {
                        Id = order.Id,
                        Tanggal = order.Tanggal,
                        NamaPelanggan = order.NamaPelanggan,
                        Harga = order.Harga,
                        WorkflowId = order.WorkflowId,
                        StatusTerakhir = order.StatusTerakhir
                    };
                    orderRequest.ListRiwayatOrder = new List<RiwayatOrderRequest>();
                    foreach (RiwayatOrder riwayatOrder in order.ListRiwayatOrder)
                    {
                        RiwayatOrderRequest riwayatOrderRequest = new RiwayatOrderRequest()
                        {
                            Id = riwayatOrder.Id,
                            Tanggal = riwayatOrder.Tanggal,
                            Nama = riwayatOrder.Nama,
                            Status = riwayatOrder.Status
                        };
                        if (riwayatOrder is RiwayatOrderPelangganMembayar)
                        {
                            riwayatOrderRequest.JenisPembayaran = 
                                (riwayatOrder as RiwayatOrderPelangganMembayar).JenisPembayaran;
                        }
                        orderRequest.ListRiwayatOrder.Add(riwayatOrderRequest);
                    }
                    result.Add(orderRequest);
                }
            }
            return result;
        }

    }
}

Terlihat cukup kompleks, bukan? Selain karena penggunaan DTO, hal ini juga ditambah lagi dengan ketidaksesuaian antara object oriented dan service oriented. Pada service oriented, saya harus menyediakan layanan tanpa memakai konsep OOP seperti polymorphism dan inheritance. Sementara itu, saya merancang business logic dalam bentuk OOP di domain class.

Langkah terakhir adalah memakai ServiceHost dari WCF untuk menjadikan proyek ini server yang menyediakan layanan web services. Untuk itu, saya mengubah kode program Program.cs menjadi seperti berikut ini:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace DataServices
{
    class Program
    {
        static void Main(string[] args)
        {            
            ServiceHost selfHost = new ServiceHost(typeof(Services), new Uri("http://localhost:8080/LatihanMultiTier"));
            try
            {
                selfHost.AddServiceEndpoint(typeof(Services), new WSHttpBinding(), "DataServices");
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;                
                selfHost.Description.Behaviors.Add(smb);
                selfHost.Open();
                Console.WriteLine("Data services sudah aktif dan siap melayani request...");
                Console.ReadLine();
                selfHost.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Telah terjadi kesalahan: {0}", ex.Message);
                selfHost.Abort();
            }
            Console.ReadLine();
        }
    }
}

Sebelum mencoba services yang disediakan oleh program ini, saya terlebih dahulu menjalankan Visual Studio sebagai administrator. Saya kemudian membuka command prompt untuk mengerjakan WcfTestClient.exe guna menguji setiap web services yang disediakan oleh proyek ini. Saya juga memastikan isi tabel diambil/diperbaharui dengan baik. Setelah yakin semuanya dalam keadaan baik, saya kemudian lanjut ke proyek berikutnya.

Membuat Workflow Services

Saya mulai dengan membuat sebuah proyek baru untuk workflow services dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Pada dialog yang muncul, saya memilih Workflow Console Application dan mengisi nama proyek dengan WorkflowServices. Saya juga perlu menambahkan referensi ke proyek Common dengan men-klik kanan nama proyek dan memilih menu Add Reference….

Proyek ini akan memanggil web services yang dibuat di proyek DataServices. Oleh sebab itu, saya perlu menambahkan referensi ke layanan yang dipublikasikan oleh proyek tersebut. Caranya adalah dengan men-klik kanan nama proyek WorkflowServices dan memilih menu Add Service Reference…. Pada dialog yang muncul, saya mengisi alamat dengan http://localhost:8080/LatihanMultiTier dan men-klik tombol Go (pastikan bahwa aplikasi pada proyek DataServices sudah dijalankan terlebih dahulu!). Setelah mengisi nama namespace dengan DataServices, saya kemudian men-klik tombol OK untuk mulai men-import web services. Setelah selesai, saya menekan tombol F6 untuk men-build ulang proyek ini.

Setelah ini, saya akan mempersiapkan custom activity untuk dipakai dalam workflow designer. Saya akan mulai dengan membuat sebuah class baru dengan nama GetWorkflowInstanceId yang isinya adalah seperti berikut ini (diambil dari WF Sample dari Microsoft):

//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------

using System;
using System.Activities;

namespace Microsoft.Samples.Activities.Statements
{

    /// <summary>
    /// This activity returns the instance id of the workflow it is executing in
    /// 
    /// We choose CodeActivity as the base type because we need access to the CodeActivityContext for getting access to the workflow instance ID.
    /// 
    /// This activity returns a System.Guid for the instance ID
    /// </summary>
    /// 
    public sealed class GetWorkflowInstanceId : CodeActivity<Guid>
    {
        protected override Guid Execute(CodeActivityContext context)
        {
            return context.WorkflowInstanceId;
        }
    }
}

Saya membutuhkan custom activity di atas karena WorkflowInstanceId harus diperoleh dari sebuah CodeActivityContext. Semoga saja suatu saat nanti WF akan menyediakan cara lain yang lebih user-friendly tanpa harus membuat custom activity.

Saya kemudian mengubah nama Workflow1.xaml menjadi RestoranServices.xaml. Saya juga turut mengubah nama class untuk workflow ini dengan mengubah nilai atribut x:Class menjadi WorkflowServices.RestoranServices. Pada saat membuka workflow designer, saya akan menemukan custom activity yang saya tambahkan beserta dengan activity untuk memanggil layanan web services yang sudah saya buat di proyek DataServices sebelumnya, seperti yang terlihat pada gambar berikut ini:

Custom activity yang dapat dipakai di workflow designer

Custom activity yang dapat dipakai di workflow designer

Garis besar rancangan workflow yang saya buat terlihat seperti pada gambar berikut ini:

Rancangan workflow secara garis besar

Rancangan workflow secara garis besar

Sequence Kasir Membuat Order Baru terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Kasir membuat order baru*

Rancangan untuk sequence *Kasir Membuat Order Baru*

Activity Receive akan menunggu client memanggil web service BuatOrder(). Activity ini memiliki property CanCreateInstance dengan nilai true sehingga ia adalah activity yang harus dipanggil pertama kali untuk membuat instance workflow baru. Activity SendReplyToReceive akan mengembalikan hasil ke client yang sebelumnya telah memanggil web service BuatOrder(). Saya memakai activity InitializeCorrelation untuk memakai id dari order yang dibuat sebagai nilai pengenal pada pemanggil web service berikutnya (anggap saja sepeti session di web).

Sequence Koki Mulai Memasak terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Koki Mulai Memasak*

Rancangan untuk sequence *Koki Mulai Memasak*

Sequence ini juga memiliki activity Receive dengan nama MulaiDimasak. Saya mengatur nilai correlation pada activity ini sehingga ia harus dipanggil setelah activity Receive sebelumnya dipanggil.

Sequence Koki Selesai Memasak tidak jauh berbeda dengan sebelumnya, terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Koki Selesai Memasak*

Rancangan untuk sequence *Koki Selesai Memasak*

Terakhir, sequence Pelanggan Membayar terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Pelanggan Membayar*

Rancangan untuk sequence *Pelanggan Membayar*

Seluruh activity Receive setelah yang pertama (yang memiliki property CanCreateInstance dengan nilai true) harus memiliki nilai correlation sehingga mereka bisa berpartisipasi dalam sebuah workflow instance (bayangkan session!) yang sama. Tanpa nilai correlation, WF tidak dapat menentukan urutan eksekusi karena web service bisa dipanggil dari berbagai client yang berbeda dan urutan yang berbeda.

Saya kemudian membuat kode program yang memakai WorkflowServiceHost untuk membuat sebuah server web services bagi workflow ini dengan mengubah kode program Program.cs menjadi seperti berikut ini:

using System;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

namespace WorkflowServices
{

    class Program
    {
        static void Main(string[] args)
        {
            RestoranServices restoranServices = new RestoranServices();
            WorkflowServiceHost host = new WorkflowServiceHost(restoranServices, 
                new Uri("http://localhost:8181/LatihanMultiTier/WorkflowServices"));                                    
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            host.Description.Behaviors.Add(smb);
            host.AddDefaultEndpoints();
            host.Open();
            Console.WriteLine("Workflow services sudah aktif dan siap melayani request...");            
            Console.ReadLine();
            host.Close();
        }
    }
}

Saya perlu memastikan bahwa port yang dipakai oleh server ini berbeda dengan port yang dipakai oleh server data services bila ingin menjalankan kedua server tersebut pada komputer yang sama.

Berikutnya, sebelum melakukan pengujian, saya perlu menjalankan proyek DataServices dan WorkflowServices (karena proyek WorkflowServices bergantung pada layanan yang disediakan proyek DataServices). Setelah itu, saya menguji web services yang disediakan oleh proyek WorkflowServices dengan menggunakan WcfTestClient.exe seperti yang terlihat pada gambar berikut ini:

Layanan web services yang disediakan oleh proyek *WorkflowServices*

Layanan web services yang disediakan oleh proyek *WorkflowServices*

Membuat User Interface Untuk Waitress

Saya mulai dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Setelah itu, saya memilih template WPF Application dan mengisi nama proyek dengan Waitress. Proyek ini akan memanggil web services yang disediakan oleh proyek WorkflowServices. Oleh sebab itu, saya menambahkan referensi ke layanan yang disediakan oleh proyek tersebut (setelah sebelumnya menjalankan proyek WorkflowServices) dengan men-klik kanan nama proyek dan memilih menu Add Service Reference…. Saya kemudian mengisi kotak dialog yang muncul agar terlihat seperti pada gambar berikut ini dan men-klik tombol OK:

Menambahkan service reference

Menambahkan service reference

Saya kemudian membuat sebuah class baru untuk mewakili view model yang isinya seperti berikut ini:

using System;
using System.Windows;
using System.Windows.Input;
using waitress.WorkflowServices;

namespace waitress
{
    public class ViewModel
    {
        public ViewModel()
        {
            BuatOrderCommand = new BuatOrderCommandImpl(this);
        }

        public DateTime Tanggal { get; set; }

        public string NamaPelanggan { get; set; }

        public decimal Harga { get; set; }

        public ICommand BuatOrderCommand { get; private set; }
    }

    public class BuatOrderCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public BuatOrderCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            ServiceClient service = new ServiceClient();

            OrderSimpan orderSimpan = new OrderSimpan();
            orderSimpan.Tanggal = viewModel.Tanggal;
            orderSimpan.NamaPelanggan = viewModel.NamaPelanggan;
            orderSimpan.Harga = viewModel.Harga;
            service.BuatOrder(ref orderSimpan);
            MessageBox.Show("Order berhasil disimpan");
        }
    }

}

Saya kemudian merancang view agar terlihat seperti berikut ini:

Contoh rancangan view untuk waitress

Contoh rancangan view untuk waitress

Saya juga tidak lupa mengisi nilai DataContext untuk view sehingga terjadi binding dengan view model yang telah saya buat. Saya memakai modus binding OneWayToSource karena form ini hanya dipakai untuk mengisi data saja.

Membuat User Interface Untuk Koki

Saya mulai dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Setelah itu, saya memilih template WPF Application dan mengisi nama proyek dengan Koki. Proyek ini akan memanggil web services yang disediakan oleh proyek WorkflowServices dan proyek DataServices (untuk mencari seluruh order yang ada berdasarkan status). Saya perlu menambahkan referensi ke dua proyek tersebut dengan men-klik kanan nama proyek dan memilih menu Add Service Reference….

Saya kemudian membuat sebuah view model yang isinya seperti berikut ini:

using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Koki.DataServices;

namespace Koki
{
    public class ViewModel
    {
        public DataServices.ServicesClient dataServices;
        public WorkflowServices.ServiceClient workflowServices;

        public ViewModel()
        {
            this.dataServices = new DataServices.ServicesClient();
            this.workflowServices = new WorkflowServices.ServiceClient();

            this.ListUntukMulaiDimasak = new ObservableCollection<OrderRequest>(
                dataServices.LihatSemuaOrder("Dipesan"));
            this.ListUntukSedangDimasak = new ObservableCollection<OrderRequest>(
                dataServices.LihatSemuaOrder("MulaiDimasak"));

            MulaiDimasakCommand = new MulaiDimasakCommandImpl(this);
            SelesaiDimasakCommand = new SelesaiDimasakCommandImpl(this);
            RefreshCommand = new RefreshCommandImpl(this);
        }

        public ObservableCollection<OrderRequest> ListUntukMulaiDimasak { get; private set; }

        public ObservableCollection<OrderRequest> ListUntukSedangDimasak { get; private set; }

        public ICommand MulaiDimasakCommand { get; private set; }

        public ICommand SelesaiDimasakCommand { get; private set; }

        public ICommand RefreshCommand { get; private set; }

        public OrderRequest SelectedMulaiDimasak { get; set; }

        public OrderRequest SelectedSelesaiDimasak { get; set; }

        public String NamaKoki { get; set; }
    }

    public class RefreshCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public RefreshCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.ListUntukMulaiDimasak.Clear();
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("Dipesan"))
            {
                viewModel.ListUntukMulaiDimasak.Add(o);
            }

            viewModel.ListUntukSedangDimasak.Clear();
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("MulaiDimasak"))
            {
                viewModel.ListUntukSedangDimasak.Add(o);
            }
        }

    }

    public class MulaiDimasakCommandImpl : ICommand 
    {
        private ViewModel viewModel;

        public MulaiDimasakCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return viewModel.SelectedMulaiDimasak != null;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.workflowServices.MulaiDimasak(new WorkflowServices.MulaiDimasak()
            {
                orderId = viewModel.SelectedMulaiDimasak.Id,
                nama = viewModel.NamaKoki,
                tanggal = DateTime.Now,                
            });

            viewModel.ListUntukMulaiDimasak.Remove(viewModel.SelectedMulaiDimasak);

            viewModel.ListUntukSedangDimasak.Clear();
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("MulaiDimasak"))
            {
                viewModel.ListUntukSedangDimasak.Add(o);
            }
        }
    }

    public class SelesaiDimasakCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public SelesaiDimasakCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return viewModel.SelectedSelesaiDimasak != null;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.workflowServices.SelesaiDimasak(new WorkflowServices.SelesaiDimasak()
            {
                orderId = viewModel.SelectedSelesaiDimasak.Id,
                nama = null,
                tanggal = DateTime.Now,                
            });
            viewModel.ListUntukSedangDimasak.Remove(viewModel.SelectedSelesaiDimasak);            
        }
    }
}

Seperti biasa, saya perlu mengisi DataContext untuk melakukan binding dengan view model. Setelah itu, saya membuat view dengan tampilan yang terlihat seperti pada gambar berikut ini:

Contoh rancangan view untuk koki

Contoh rancangan view untuk koki

Membuat User Interface Untuk Kasir

Saya mulai dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Setelah itu, saya memilih template WPF Application dan mengisi nama proyek dengan Kasir. Saya juga menambahkan referensi ke web services yang disediakan oleh proyek WorkflowServices dan proyek DataServices seperti pada proyek sebelumnya.

Saya kemudian membuat sebuah view model yang isinya seperti berikut ini:

using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Kasir.DataServices;
using Kasir.WorkflowServices;

namespace Kasir
{
    public class ViewModel
    {
        public DataServices.ServicesClient dataServices;
        public WorkflowServices.ServiceClient workflowServices;

        public ViewModel()
        {
            this.dataServices = new DataServices.ServicesClient();
            this.workflowServices = new WorkflowServices.ServiceClient();

            this.ListUntukDibayar = new ObservableCollection<OrderRequest>(
                dataServices.LihatSemuaOrder("SelesaiDimasak"));

            this.BayarCommand = new BayarCommandImpl(this);
            this.RefreshCommand = new RefreshCommandImpl(this);
        }

        public ObservableCollection<OrderRequest> ListUntukDibayar { get; private set; }

        public OrderRequest SelectedDibayar { get; set; }

        public ICommand BayarCommand { get; private set; }

        public ICommand RefreshCommand { get; private set; }

        public String JenisPembayaran { get; set; }
    }

    public class RefreshCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public RefreshCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.ListUntukDibayar.Clear();                        
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("SelesaiDimasak"))
            {
                viewModel.ListUntukDibayar.Add(o);
            }
        }
    }

    public class BayarCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public BayarCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return viewModel.SelectedDibayar != null && viewModel.JenisPembayaran != null;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            PelangganMembayar pelangganMembayar = new PelangganMembayar()
            {
                orderId = viewModel.SelectedDibayar.Id,
                tanggal = viewModel.SelectedDibayar.Tanggal,
                jenisPembayaran = viewModel.JenisPembayaran
            };
            viewModel.workflowServices.PelangganMembayar(pelangganMembayar);
            viewModel.ListUntukDibayar.Remove(viewModel.SelectedDibayar);
        }
    }
}

Rancangan view yang saya buat terlihat seperti pada gambar berikut ini:

Contoh rancangan view untuk kasir

Contoh rancangan view untuk kasir

Menjalankan Aplikasi

Sampai disini, saya telah memiliki 6 proyek di solution Visual Studio, seperti yang terlihat pada gambar berikut ini:

Daftar proyek untuk solution di Visual Studio

Daftar proyek untuk solution di Visual Studio

Pada saat aplikasi dijalankan, komunikasi antar-tier akan terlihat seperti pada gambar berikut ini:

Arsitektur aplikasi n-tier (multi-tier)

Arsitektur aplikasi n-tier (multi-tier)

Contoh data yang tersimpan di database adalah:

Contoh isi tabel

Contoh isi tabel

Setiap tier dapat dijalankan pada sebuah komputer (server) tunggal. Mereka akan saling berkomunikasi dengan menggunakan web services. Contoh kasus yang lebih realistis adalah presentation tier untuk kasir dalam bentuk aplikasi mobile yang dijalankan di smartphone, presentation tier untuk koki dalam bentuk aplikasi WPF yang dijalankan di perangkat touch screen, presentation tier untuk kasir dalam bentuk aplikasi Windows Forms yang dijalankan di PC yang dilengkapi dengan printer.

Penggunaan WF pada contoh aplikasi sederhana ini memang terlalu berlebihan, namun ada beberapa keuntungan yang dapat diperoleh. Developer dapat memperoleh visualisasi workflow sehingga mereka dapat lebih memahami kasus yang dihadapi. Selain itu, WF menjaga urutan eksekusi setiap activity Receive. Dengan demikian, client akan memperoleh pesan kesalahan bila ia memanggil web service PelangganMembayar() sebelum ada yang memanggil SelesaiDimasak() pada workflow instance yang sama.

Membuat Web Service Dengan Windows Communication Foundation (WCF)

Sebuah layer dalam software engineering adalah pemisahan secara logika dalam mengelompokkan kode program. Biasanya pengelompokkan ini dilakukan berdasarkan peran dan fungsi kode program. Berbeda dengan layer, tier adalah kode program yang berjalan di sebuah fisik yang berbeda, misalnya di komputer server. Tier dibedakan berdasarkan fisik (seperti komputer server) ini. Biasanya, sebuah tier diwakili oleh unit eksekusi (seperti file exe) yang berbeda. Sebagai contoh, bila terdapat A.EXE, B.EXE, dan C.EXE sebagai bagian dari sebuah aplikasi di komputer yang sama dimana mereka saling berkomunikasi melalui mekanisme Interprocess Communication (IPC) seperti shared memory, maka mereka boleh dianggap sebagai tier yang berbeda.

Windows Communication Foundation (WCF) adalah fasilitas dari .NET Framework untuk mempermudah komunikasi antar-tier. Sebagai latihan, saya akan mengubah kode program yang saya buat di artikel sebelumnya, Melakukan Binding Collection Di WPF, sehingga kode program tersebut murni hanya merupakan presentation tier dengan WPF. Nantinya, ia akan mengakses business service tier yang berada di aplikasi berbeda. Domain model yang sebelumnya dipakai langsung di aplikasi WPF tersebut akan saya pindahkan ke business service tier karena logika bisnis akan dikerjakan disini (pada latihan sederhana saya, logika bisnis-nya adalah method Total() yang menghitung total setelah diskon).

Saya akan mulai dengan membuat sebuah proyek console application Visual C# yang saya beri nama LatihanBusinessLogicTier. Untuk memakai WCF, saya perlu men-klik kanan nama proyek, kemudian memilih Add Reference…. Pada tab .NET, saya memilih System.ServiceModel dan men-klik tombol OK. Selain itu, saya perlu melakukan langkah yang sama untuk menambahkan referensi ke System.Runtime.Serialization agar nantinya saya bisa memakai atribut seperti [DataContract] dan [DataMember].

Saya kemudian memindahkan ItemPenjualan.cs dari proyek sebelumnya ke proyek baru ini. Setelah itu, saya mengubah namespace dari LatihanMVVM menjadi LatihanBusinessLogicTier. Mengapa domain model diletakkan disini? Bila seandainya logika bisnis (pada contoh ini menghitung Total()) diletakkan di presentation tier yang memakai WPF lalu suatu saat nanti ada presentation tier baru yang berbasis web, maka kode program Total() harus diduplikasi. Ini bukanlah sesuatu yang baik. Oleh sebab itu, saya membuat presentation tier menjadi sebuah aplikasi‘dummy’ yang tidak tahu apa-apa selain bertugas menampilkan.

Walaupun domain model dikerjakan oleh business service tier, object-nya tidak akan dikembalikan begitu saja ke presentation tier. Dengan kata lain, sebaiknya business service tier tidak mengembalikan instance dari ItemPenjualan secara langsung ke presentation tier. Mengapa demikian? Lazy loading tidak akan bekerja dengan baik karena presentation tier tidak lagi memiliki akses ke database. Karena lazy loading tidak bekerja, maka tidak semua method di domain model dapat dikerjakan. Lagi pula, presentation tier tidak perlu memanggil method yang mewakili business logic tersebut. Sebaiknya ia hanya memperoleh nilai dan menampilkannya (atau sebaliknya, mengirim nilai).

Oleh sebab itu, saya perlu membuat class yang akan dipakai oleh data transfer object (DTO). Sebuah class DTO hanya memiliki property tanpa method. Walaupun property-nya hampir sama seperti milik domain model, mereka biasanya terbatas untuk keperluan service tertentu. Sebagai contoh, navigation property (misalnya mengakses Penjualan dari ItemPenjualan) tidak disertakan bila tidak dibutuhkan oleh client. Beberapa programmer merasa bahwa penggunaan DTO terlalu berlebihan untuk kasus yang sederhana. Yup! Sebenarnya, saya dapat langsung men-share domain model ke client untuk contoh sederhana ini. Tapi pada aplikasi yang lebih besar dan lebih rumit (dimana domain model memiliki navigation property dan asosiasi seperti one-to-many), penggunaan DTO merupakan sesuatu yang disarankan.

Setelah menemukan rancangan arsitektur yang akan dipakai, saya siap untuk membuat kode program. Saya akan mulai dengan membuat sebuah interface baru yang mewakili service contract dengan nama ItemPenjualanService yang isinya seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;

namespace LatihanBusinessLogicTier
{
    [ServiceContract]
    public class ItemPenjualanService
    {
        [OperationContract]
        public void Simpan(ItemPenjualanSaveDTO itemPenjualanDTO)
        {
            // Kode Program Disini!
        }

        [OperationContract]
        public DaftarItemPenjualanDTO LihatSemua()
        {
            // Kode Program Disini!
        }
    }

    [DataContract]
    public class ItemPenjualanDTO
    {
        [DataMember]
        public long Id;

        [DataMember]
        public string NamaBarang;

        [DataMember]
        public int Jumlah;

        [DataMember]
        public decimal Harga;

        [DataMember]
        public decimal DiskonPersen;

        [DataMember]
        public decimal Total;
    }

    [CollectionDataContract]
    public class DaftarItemPenjualanDTO : List<ItemPenjualanDTO> { }

    [DataContract]
    public class ItemPenjualanSaveDTO
    {
        [DataMember]
        public string NamaBarang;

        [DataMember]
        public int Jumlah;

        [DataMember]
        public decimal Harga;

        [DataMember]
        public decimal DiskonPersen;
    }
}

Pada kode program di atas, DTO untuk operasi Simpan() tidak memiliki property Id dan Total karena memang tidak dibutuhkan untuk operasi tersebut. Dengan demikian, sebuah DTO tidak selalu memiliki property yang sama dengan domain class yang diwakilinya.

Kode program di atas belum selesai. Saya perlu mengimplementasikan isi service untuk kode program di atas, tapi sebelumnya saya akan terlebih dahulu membuat persistence layer.

Langkah selanjutnya adalah menambahkan Entity Framework sebagai persistence layer yang akan mengakses database. Untuk itu, saya men-install Entity Framework terbaru di proyek dengan memilih menu Project, Manage NuGet Packages…. Setelah Entity Framework berhasil ditambahkan pada proyek, saya perlu mengubah App.config sama seperti pada proyek di artikel sebelumnya. Saya juga perlu men-klik kanan nama proyek dan memilih Add Reference…, .NET, MySql.Data.Entity for EF6 agar dapat memakai database MySQL dengan Entity Framework 6. Berikutnya, saya men-copy file LatihanContext.cs dan MyHistoryContext.cs dari proyek di artikel sebelumnya ke proyek baru ini. Saya perlu mengubah namespace yang dipakai oleh kedua file tersebut dari LatihanMVVM menjadi LatihanBusinessLogicTier. Setelah ini persistence layer sudah selesai dibuat dan siap dipakai.

Saya akan membuat implementasi untuk service Simpan() di ItemPenjualanService.cs yang isinya adalah seperti berikut ini:

...
[ServiceContract]
public class ItemPenjualanService
{

  [OperationContract]
  public void Simpan(ItemPenjualanSaveDTO itemPenjualanDTO)
  {
    using (var db = new LatihanContext())
    {
      ItemPenjualan p = new ItemPenjualan()
      {
        NamaBarang = itemPenjualanDTO.NamaBarang,
        Jumlah = itemPenjualanDTO.Jumlah,
        Harga = itemPenjualanDTO.Harga,
        DiskonPersen = itemPenjualanDTO.DiskonPersen
      };
      db.DaftarItemPenjualan.Add(p);
      db.SaveChanges();
    }
  }

  ...
}
...

Saya juga mengimplementasikan service LihatSemua() yang isinya seperti berikut ini:

...
[ServiceContract]
public class ItemPenjualanService
{

  ...
  [OperationContract]
  public DaftarItemPenjualanDTO LihatSemua()
  {
    DaftarItemPenjualanDTO hasil = new DaftarItemPenjualanDTO();
    using (var db = new LatihanContext())
    {
      foreach (var p in db.DaftarItemPenjualan)
      {
        hasil.Add(new ItemPenjualanDTO()
        {
          Id = p.Id.Value,
          NamaBarang = p.NamaBarang,
          Harga = p.Harga,
          DiskonPersen = p.DiskonPersen,
          Total = p.Total()
        });
     }
   }
   return hasil;
  }
  ...
}
...

Saya juga membuat sebuah service HitungTotal() yang akan mengembalikan sebuah decimal seperti berikut ini:

...
[ServiceContract]
public class ItemPenjualanService
{

  ...
  [OperationContract]
  public decimal HitungTotal(ItemPenjualanSaveDTO itemPenjualanDTO)
  {
    ItemPenjualan p = new ItemPenjualan()
    {
       NamaBarang = itemPenjualanDTO.NamaBarang,
       Jumlah = itemPenjualanDTO.Jumlah,
       Harga = itemPenjualanDTO.Harga,
       DiskonPersen = itemPenjualanDTO.DiskonPersen
    };
    return p.Total();
  }
  ...
}
...

Pada langkah berikutnya, saya perlu men-host web services yang sudah saya buat. Agar sederhana, saya akan memakai fasilitas self-hosting. Pada prakteknya, web services sebaiknya di-host ke web server seperti Internet Information Services (IIS). Untuk membuat aplikasi console yang dapat melayani request (self-hosting), saya akan membuat kode program berikut ini pada file Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace LatihanBusinessLogicTier
{
    class Program
    {
        static void Main(string[] args)
        {
            Uri baseAddress = new Uri("http://localhost:80/LatihanBusinessLogicTier/Service");
            ServiceHost selfHost = new ServiceHost(typeof(ItemPenjualanService), baseAddress);
            try
            {
                selfHost.AddServiceEndpoint(typeof(ItemPenjualanService), new WSHttpBinding(), "ItemPenjualanService");
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;                
                selfHost.Description.Behaviors.Add(smb);
                selfHost.Open();
                Console.WriteLine("Business Logic Tier sudah aktif dan siap melayani request.");
                while (true)
                {
                    System.Threading.Thread.Sleep(50);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Telah terjadi kesalahan: {0}", ex.Message);
                selfHost.Abort();
            }
            Console.ReadLine();
        }
    }
}

Salah satu kelebihan WCF adalah ia bisa mendukung satu atau lebih binding. Pada kosa kata WCF, binding adalah sesuatu yang menentukan bagaimana proses komunikasi terjadi (protokol yang dipakai, format data, dan sebagainya). Contoh binding yang disediakan oleh .NET saat ini adalah WSHttpBinding untuk web services berbasis SOAP, WebHttpBinding yang dapat dipakai untuk membuat web services berbasis REST, NetTcpBinding untuk komunikasi jaringan melalui socket, dan sebagainya. Programmer juga dapat mengimplementasikan binding baru bila perlu. Menariknya, sebuah aplikasi boleh memiliki lebih dari satu binding. Salah contoh penerapannya, sebuah aplikasi yang sama dapat men-ekspos services yang dimilikinya dalam bentuk REST maupun SOAP. Pada contoh ini, saya hanya memakai WsHttpBinding sehingga services akan dipublikasi dalam bentuk layanan SOAP yang mendukung spesifikasi WS-* .

Sekarang saatnya menjalankan program yang berisi web services ini. Tapi, sebelumnya, saya perlu menutup Visual Studio dan menjalankan dengan memilih Run As Administrator. Hal ini karena bila program tersebut dijalankan oleh user yang tidak memiliki hak akses, akan muncul pesan kesalahan HTTP could not register URL ... Your process does not have access rights to this namespace.

Bila saya menjalankan program, saya akan memperoleh pesan seperti berikut ini:

Business Logic Tier sudah aktif dan siap melayani request.

Untuk menguji apakah services dapat diakses dengan baik, saya akan membuka Visual Studio Command Prompt (2010) dan memberikan perintah WcfTestClient. Pada aplikasi WcfTestClient yang muncul, saya memilih menu File, Add Service…. Lalu pada dialog pengisian yang muncul, saya mengisi dengan nilai http://localhost/LatihanBusinessLogicTier/Service dan men-klik tombol OK. Saya akan memperoleh hasil seperti pada gambar berikut ini:

Menguji web services yang telah dibuat

Menguji web services yang telah dibuat

Saya akan mencoba mengerjakan service LihatSemua() dengan men-double click nama service tersebut. Kemudian pada bagian kanan, saya men-klik tombol Invoke. Saya akan memperoleh hasil seperti pada gambar berikut ini:

Memanggil service `LihatSemua()`

Memanggil service `LihatSemua()`

Saya juga dapat mencoba mengerjakan service Simpan() seperti yang terlihat pada gambar berikut ini:

Memanggil service `Simpan()`

Memanggil service `Simpan()`

Service Simpan() tidak mengembalikan respon berupa sebuah nilai. Untuk memastikan apakah ia berhasil dikerjakan dengan baik, saya perlu memeriksa database secara manual untuk melihat apakah data pada tabel sudah bertambah.

Setelah memastikan bahwa business service tier sudah beres, saya akan memperbaharui presentation tier agar memanggil business service tier. Saya perlu menghapus file yang berkaitan dengan Entity Framework karena kini presentation tier tidak lagi mengakses database secara langsung. Saya juga perlu membuang domain model yang sudah saya buat sebelumnya (file ItemPenjualan.cs) karena presentation tier tidak lagi menampung logika bisnis. Yang tersisa hanya dua komponen untuk MVVM, yaitu view (file MainWindow.xaml) dan view model (file ItemPenjualanViewModel.cs). Lalu, mana model-nya? MVVM tidak akan pernah lengkap tanpa model, bukan?

Yang kini menjadi model dalam MVVM disini adalah DTO yang dikembalikan oleh business service tier. Muncul sebuah pertanyaan disini: Class DTO didefinisikan di business service tier, apakah saya perlu mereferensi class tersebut dengan menyertakan proyek business service tier sebagai dependency? Tidak harus! Lalu, class apa yang harus saya pakai? Karena class DTO hanya berisi data tanpa logika program, maka saya bisa menghasilkan class tersebut secara otomatis selama business service tier mempublikasikan metadata untuk services yang dimilikinya. Dengan demikian, saya tidak perlu mereferensikan proyek business service tier di proyek presentation tier sehingga dapat mengurangi ketergantungan.

Untuk menghasilkan DTO dan kode program pembantu lainnya secara otomatis, saya men-klik kanan padaService References (bukan References!) di Solution Explorer, lalu memilih Add Service Reference…. Saya memastikan terlebih dahulu bahwa program untuk business service tier sudah dijalankan, kemudian mengisi dialog yang muncul seperti pada gambar berikut ini:

Menghasilkan kode program dan DTO di client berdasarkan metadata

Menghasilkan kode program dan DTO di client berdasarkan metadata

Bila saya melihat di Object Browser, saya akan menemukan beberapa class baru yang dibuat oleh Visual Studio 2010 secara otomatis untuk saya, seperti yang terlihat di gambar berikut ini:

Class yang dihasilkan

Class yang dihasilkan

Class DTO yang dihasilkan hampir mirip seperti asli-nya di proyek business service tier, hanya saja sudah dilengkapi dengan implementasi INotifyPropertyChanged. Dengan demikian, sebenarnya DTO dapat langsung dilewatkan oleh view model ke view (binding langsung dari view ke model DTO). Selain itu, saya juga akan menemukan interface ItemPenjualanService dan ItemPenjualanServiceChannel serta sebuah class ItemPenjualanServiceClient. Saya dapat mengakses web services yang disediakan business service tier melalui class tersebut.

Langkah terakhir adalah mengubah view model (file ItemPenjualanViewModel.cs) agar memakai DTO yang dihasilkan sehingga kode programnya menjadi seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows;
using System.Collections.ObjectModel;
using LatihanMVVM.BusinessLogicService;


namespace LatihanMVVM
{
    public class ItemPenjualanViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private ItemPenjualanSaveDTO model;

        private ICommand simpanCommand;        

        private ItemPenjualanServiceClient itemPenjualanService;

        private ObservableCollection<ItemPenjualanDTO> listItemPenjualan;

        public ItemPenjualanViewModel(ItemPenjualanSaveDTO itemPenjualan = null)
        {
            this.model = itemPenjualan ?? new ItemPenjualanSaveDTO();            

            this.itemPenjualanService = new ItemPenjualanServiceClient();
            this.ListItemPenjualan = new ObservableCollection<ItemPenjualanDTO>(itemPenjualanService.LihatSemua());
        }


        public ObservableCollection<ItemPenjualanDTO> ListItemPenjualan 
        {
            get { return this.listItemPenjualan; }
            private set { this.listItemPenjualan = value; }
        }

        public string NamaBarang
        {
            get { return model.NamaBarang; }
            set
            {
                if (value != model.NamaBarang)
                {
                    model.NamaBarang= value;
                    PropertyChanged(this, new PropertyChangedEventArgs("NamaBarang"));
                }
            }
        }

        public int Jumlah
        {
            get { return model.Jumlah; }
            set
            {
                if (value != model.Jumlah)
                {
                    model.Jumlah= value;
                    PropertyChanged(this, new PropertyChangedEventArgs("Jumlah"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));
                }
            }
        }

        public decimal Harga
        {
            get { return model.Harga; }
            set
            {
                if (value != model.Harga)
                {
                    model.Harga = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("Harga"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));                     
                }
            }
        }

        public decimal DiskonPersen
        {
            get { return model.DiskonPersen; }
            set
            {
                if (value != model.DiskonPersen)
                {
                    model.DiskonPersen = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("DiskonPersen"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));
                }
            }
        }

        public string Total
        {
            get 
            {
                decimal? total = itemPenjualanService.HitungTotal(model);
                if (!total.HasValue)
                {
                    return "-";
                }
                else
                {
                    return total.Value.ToString("C");
                }
            }
        }

        public ItemPenjualanSaveDTO Model
        {
            get { return this.model; }
        }

        public ItemPenjualanService ItemPenjualanService
        {
            get { return this.itemPenjualanService; }
        }

        public ICommand SimpanCommand
        {
            get
            {
                if (this.simpanCommand == null)
                {
                    this.simpanCommand = new SimpanCommand(this);
                }
                return this.simpanCommand; 
            }
        }

    }


    public class SimpanCommand : ICommand
    {

        private ItemPenjualanViewModel viewModel;

        public SimpanCommand(ItemPenjualanViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return !viewModel.Total.Equals("Rp0");
        }

        public void Execute(object parameter)
        {            
            viewModel.ItemPenjualanService.Simpan(viewModel.Model);            
            MessageBox.Show("Data berhasil disimpan ke database");
        }

    }

}

Sebenarnya view model dapat meng-ekpos DTO secara langsung untuk dipakai oleh view. Akan tetapi, saya berusaha menjaga agar tidak perlu ada perubahan pada view sehingga saya hanya perlu mengubah view model saja. Bila aplikasi dijalankan, ia akan bekerja sama seperti sebelumnya, tapi kali ini business logic dan database bisa saja berada di komputer terpisah, seperti yang terlihat pada gambar berikut ini:

Arsitektur Program

Arsitektur Program

Membuat RESTful Web Service Dengan Spring Data REST

Pada tulisan tentang web service sebelumnya, saya selalu menggunakan SOAP. Selain memakai SOAP, solusi yang kini populer adalah REST. Yup! Saya sering berpikir SOA telah terkubur karena mulai jarang diperbicarakan. Tapi ternyata masih ada buku baru Thomas Erl yang diterbitkan pada Agustus 2012 yang berjudul “SOA with REST: Principles, Patterns & Constraints for Building Enterprise Solutions with REST“.

Saya tidak akan membicarakan teori disini, tapi apa kelebihan REST? Bila SOAP adalah “early-binding” (bayangkan sebuah variabel dengan tipe data yang ketat dan sudah pasti!) maka REST adalah “late-binding” (bayangkan variabel generik tanpa tipe data seperti di JavaScript). REST lebih ringan karena bisa dipakai cukup dengan memanggil request HTTP. REST juga tidak perlu memakai WSDL untuk mengetahui kontrak servis! Loh, jika tidak ada WSDL, lalu bagaimana mengetahui layanan apa yang disediakan? REST mensyaratkan penggunaan URI dan Request Method yang harus dipatuhi. Sebagai contoh:

REST pada dasarnya adalah mengakses HTTP layaknya browsing.  Dengan demikian, tidak dibutuhkan “kode program” khusus.  Semua bahasa yang mendukung akses HTTP dapat memakai REST.  Yang membedakan REST adalah pada REST terdapat peraturan-peraturan yang harus “dipatuhi” sehingga komunikasi bisa berjalan lancar.

Walaupun demikian, REST bukanlah protokol seperti SOAP, sehingga “peraturan”-nya boleh dilanggar.  Hal ini dapat membingungkan client yang memakai.  Yup! Kadang-kadang sulit menjelaskan pada mahasiswa yang berharap menemukan kode program atau teknik baru yang mutakhir;  REST berhubungan dengan disiplin dan ‘manajemen’.  Mahasiswa yang berfokus pada membuat sistem untuk skripsi lalu meninggalkannya begitu saja setelah lulus akan sulit merasakan manfaatnya.

Saya akan mulai membuat RESTful web service dengan memakai Spring Data REST. Saya mulai dengan membuat sebuah proyek baru dengan memilih File, New, Spring Template Project di Spring Tool Suite (STS).  Pada dialog yang muncul, saya memilih Spring MVC Project dan men-klik tombol Next.  Saya mengisi nama proyek dengan rest-api dan lokasi package dengan com.jocki.rest.  Lalu, saya men-klik tombol Finish.

Setelah proyek selesai dibuat, saya me-double click file pom.xml untuk memastikan bahwa properties org.springframework-version minimal adalah versi 3.0. Selain itu, saya akan menambahkan dependency berikut ini:

  • org.hibernate : hibernate-entitymanager : 3.6.0.Final (sebagai JPA provider)
  • org.springframework.data : spring-data-jpa : 1.1.0.RELEASE
  • org.springframework : spring-tx : ${org.springframework-version}
  • org.springframework : spring-orm : ${org.springframework-version}
  • org.springframework.data : spring-data-rest-webmvc : 1.0.0.RELEASE
  • com.h2database : h2 : 1.3.167 (sebagai database in-memory)

Pada Package Explorer, saya men-klik kanan folder src/main/java, men-klik kanan dan memilih menu New, Class. Saya mengisi Package dengan com.jocki.domain, dan mengisi Name dengan Buku. Class ini akan mewakili domain class saya, dimana isi kode programnya adalah:

package com.jocki.domain;

import java.io.Serializable;

import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Cacheable
public class Buku implements Serializable {

	private static final long serialVersionUID = 9099989372502423316L;

	@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
	private Long id;

	private String judul;

	private String isbn;

	private Integer harga;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getJudul() {
		return judul;
	}

	public void setJudul(String judul) {
		this.judul = judul;
	}

	public String getIsbn() {
		return isbn;
	}

	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}

	public Integer getHarga() {
		return harga;
	}

	public void setHarga(Integer harga) {
		this.harga = harga;
	}

}

Untuk melakukan operasi persistensi (CRUD ke database embedded H2), saya akan menggunakan Spring Data JPA. Untuk itu, saya men-klik kanan pada folder src/main/java, kemudian memilih New, Interface. Pada Package, saya mengisi com.jocki.repository dan pada Name, saya mengisi BukuRepository. Kode program untuk interface tersebut akan terlihat seperti berikut ini:

package com.jocki.respository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.jocki.domain.Buku;

public interface BukuRepository extends JpaRepository<Buku, Long>{

}

Saya akan mulai dengan membuka folder src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml. Lalu, saya akan menambahkan definisi bean berikut ini:

<beans:bean class="org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration" />

Bean tersebut dibutuhkan untuk men-export seluruh repository saya secara otomatis sehingga operasi persistensi data pada domain object Buku dapat diakses melalui REST.

Berikutnya, saya perlu menyiapkan konfigurasi persistensi data. Saya akan mengubah file src/main/webapp/WEB-INF/spring/root-context.xml. Saya mendouble-click file ini, kemudian memilih tab Namespaces dan memastikan bahwa saya memberi tanda centang pada beans, context, jdbc, jpa, dan tx. Lalu saya menambahkan definisi berikut ini pada file root-context.xml:

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  <property name="entityManagerFactory" ref="emf" /> 
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="jpaVendorAdapter"> 
     <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
  </property> 
  <property name="packagesToScan" value="com.jocki.domain" /> 
  <property name="jpaProperties">
    <props>
      <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
      <prop key="hibernate.hbm2ddl.auto">create-drop</prop>
      <prop key="hibernate.id.new_generator_mappings">true</prop>
    </props>
  </property>
</bean>
<context:annotation-config />
<context:component-scan base-package="com.jocki" />
<jpa:repositories base-package="com.jocki.repository" />
<jdbc:embedded-database id="dataSource" type="H2" />

Setelah ini, saya tinggal menjalankan tc Server.  Caranya adalah dengan men-klik kanan pada nama proyek, memilih menu Run As, Run on server.  Pada dialog yang muncul, saya memilih salah satu server yang tersedia, misalnya tc Server v2.7, lalu men-klik tombol Finish. Setelah server selesai dijalankan, browser secara otomatis akan terbuka pada URL http://localhost:8080/rest.

Spring Data REST mendukung HATEOAS (Hypermedia as the Engine of Application State) dimana terdapat links dari sebuah resource ke resource lainnya secara konsisten.  Yang muncul pertama kali di URL tersebut adalah seluruh daftar resource yang disediakan oleh Spring Data RESTseperti berikut ini:

{
  "links" : [ {
    "rel" : "buku",
    "href" : "http://localhost:8080/rest/buku"
  } ],
  "content" : [ ]
}

REST tidak mensyaratkan penggunaan format tertentu untuk konten.  Client dapat memilih format dengan menyertakan header Content-Type yang diinginkan seperti application/json atau application/xml.  Tapi untuk saat ini, Spring Data REST secara bawaan (tanpa perubahan!) hanya akan mengembalikan format application/json.

Untuk menguji REST, saya akan memakai tool curl.

Untuk melihat daftar seluruh resource “buku” yang ada di database,  saya perlu memberikan method GET pada URL http://localhost:8080/rest/buku seperti yang terlihat pada gambar berikut ini:

Memakai REST untuk melihat seluruh "buku" yang tersedia.

Operasi REST untuk melihat seluruh “buku” yang tersedia.

Karena saya menurunkan interface BukuRepository dari JpaRepository, maka secara otomatis saya memiliki fitur penghalamanan (paging).

Untuk melihat method apa saja yang didukung di RESTful web services, saya perlu memberikan request HTTP dengan method OPTIONS seperti yang terlihat pada gambar berikut ini:

Melihat operasi yang didukung oleh RESTful API

Melihat operasi yang didukung oleh RESTful API

Sekarang, saya akan menambah sebuah buku baru.  Untuk itu saya meng-hit URL dengan method POST seperti pada gambar berikut ini:

Membuat resource baru dengan REST

Membuat resource baru dengan REST

Saya terpaksa memberikan perintah tersebut di console Bash karena selalu gagal di console Windows.  Sekarang, bila saya melihat daftar “Buku” yang ada, saya akan menemukan hasil seperti pada gambar berikut ini:

Melihat seluruh "Buku" yang ada dengan REST

Melihat seluruh “Buku” yang ada dengan REST

Untuk menghapus buku dengan id 1, saya bisa melakukan request HTTP dengan method DELETE, seperti yang terlihat pada gambar berikut ini:

Menghapus resource dengan REST

Menghapus resource dengan REST

Perhatikan bahwa respon yang dikembalikan adalah 204 No Content, bukan 404 Not Found dan sebagainya.  Respon tersebut menunjukkan bahwa operasi hapus telah berhasil dilakukan dan tidak ada sesuatu yang perlu dikembalikan pada client.

Bagi yang ingin memakai tools berbasis GUI, bisa mencoba plugin Firefox seperti plugin RESTClient.  Tools ini mendukung authentication dengan OAuth2, sebuah pengamanan yang umum dipakai untuk RESTful web services.  Berikut ini adalah contoh tampilan plugin RESTClient di Firefox:

Tampilan plugin RESTClient di Firefox

Tampilan plugin RESTClient di Firefox

Pada prakteknya, tentu saja operasi REST tidak dipanggil melalui cURL atau plugin Firefox, melainkan oleh sistem lain atau aplikasi lain.  Hampir semua bahasa pemograman bisa melakukan request HTTP secara bawaan.   Beberapa bahkan sudah menyediakan akses REST secara mudah, misalnya Jersey di Java atau Zend_Rest_Client di PHP (yang ini  hanya mendukung XML dan banyak “melanggar” prinsip REST).  Seorang mahasiswa yang baru belajar mungkin akan bertanya, kenapa harus selalu melibatkan sistem lain?  Karena pada dasarnya web service dan SOA adalah mengenai komunikasi antar-sistem atau antar-program!  Penerapan pada sebuah sistem tunggal tanpa komunikasi cukup disebut SOP (Service Oriented Programming).

Spring Data REST memang mempermudah meng-export repository menjadi RESTful Web Services.  Tetapi RESTful Web Services tidak hanya berisi operasi CRUD saja, melainkan juga services (business logic).  Untuk mendapatkan kendali yang lebih penuh, RESTful API dapat dibuat dari Spring Web MVC controller yang telah dilengkapi annotation yang mendukung REST.

Mengakses Web Services Dari Aplikasi Java Micro Edition (Mobile)

Artikel ini adalah request dari Erna

Seorang mahasiswi bertanya bagaimana cara mengakses database yang ada di server melalui aplikasi Java Micro Edition (JME) di ponsel.  Salah satu cara yang dapat ditempuh adalah dengan menggunakan web services.  Pada tulisan ini, saya akan memakai WS-* Web Services.  Alternatif lain adalah memakai web service RESTfulyang lebih ringan.

Membuat Web Service dengan Java Di Sisi Server

Web Service akan dijalankan pada komputer server yang terhubung dengan database.   Saya akan menggunakan NetBeans IDE. Langkah pertama adalah memilih menu File, New Project…  Kemudian pilih Java Web, Web Application.  Klik tombol Next. Berikan sebuah nama proyek seperti LatihanWebService, kemudian klik tombol Next.

Pada langkah berikutnya, saya memakai server GlassFish Server 3.1.2 dan versi Java EE 6 Web.  Saya membiarkan context path saya berupa /LatihanWebService.  Karena saya tidak akan memakai framework, maka saya langsung men-klik tombol Finish.

Klik kanan pada nama proyek, kemudian pilih New, Web Service…  Pada Web Service Name, isi dengan AksesDatabase.  Kemudian pada package, isi dengan co.id.jocki.webservice.  Kemudian klik tombol Finish.

Pada nama proyek, akan ada folder Web Services yang didalamnya terdapat node AksesDatabase.  Klik kanan pada AksesDatabase, kemudian pilih Add Operation…  Isi Name dengan lihatIsiTabel, dan isi Return Type dengan java.util.ArrayList<String>.  Klik tombol OK.

Saya akan menambah sebuah operasi lagi.  Untuk itu, saya kembali men-klik kanan pada AksesDatabase, kemudian saya memilih Add Operation…  Kali ini saya mengisi dialog yang muncul sehingga terlihat seperti berikut ini:

Membuat operasi Web Service

Membuat operasi Web Service

Klik tombol OK untuk menutup dialog.

Karena saya akan memakai database MySQL, saya perlu menambahkan MySQL connector terlebih dahulu.  Saya men-klik kanan pada nama proyek (LatihanWebService), lalu memilih Properties.  Kemudian saya memilih Libraries, Add Library…  Pada pilihan yang muncul, saya memilih MySQL JDBC Driver, kemudian saya men-klik tombol Add Library.  Klik tombol OK untuk menutup dialog.

Jika saya men-double klik pada AksesDatabase di Projects, NetBeans akan memunculkan kode program dimana saya bisa menuliskan implementasi.  Saya akan memakai kode program Java 7, sehingga untuk menjalankan program ini dibutuhkan Java JDK minimal versi 7.  Java versi terbaru dapat di-download di http://www.oracle.com/technetwork/java/javase/downloads/index.html.  Selain itu, saya men-klik kanan nama proyek, memilih Properties.  Pada Sources, saya memastikan bahwa JDK 7 sedang dipilih di bagian Source/Binary Format.

Berikut ini adalah isi file AksesDatabase.javayang telah saya modifikasi:

package co.id.jocki.webservice;
import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource;
import java.sql.*;
import java.util.ArrayList;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;

@WebService(serviceName = "AksesDatabase")
public class AksesDatabase {
  private MysqlConnectionPoolDataSource dataSource;

  public AksesDatabase() {
    dataSource = new MysqlConnectionPoolDataSource();
    dataSource.setUser("jocki");
    dataSource.setPassword("password");
    dataSource.setURL("jdbc:mysql://localhost:3306/latihan"); 
  }

  private void buatTabelBilaBelumAda(Connection cn) throws SQLException {
    ResultSet rs = cn.getMetaData().getTables(null, null, "namatabel", null);
    if (rs.next()==false) {
      // Tidak ada tabel dengan nama 'namatabel', maka buat tabel tersebut
      cn.createStatement().execute("CREATE TABLE namatabel (data VARCHAR(255))");
    }
  }
  @WebMethod(operationName = "lihatIsiTabel")
  public ArrayList<String> lihatIsiTabel() {
    ArrayList<String> lstReturn = new ArrayList<>(); 
    try (Connection cn = dataSource.getConnection()) {
      buatTabelBilaBelumAda(cn);
      Statement stmt = cn.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT data FROM namatabel");
      while (rs.next()) {
        lstReturn.add(rs.getString("data"));
      }
    } catch (Exception ex) {
      System.out.println("Kesalahan [" + ex.getMessage() + "]");
    } 
    return lstReturn;
  }
  @WebMethod(operationName = "tambah")
  public boolean tambah(@WebParam(name = "nilai") String nilai) { 
    try (Connection cn = dataSource.getConnection()) {
      buatTabelBilaBelumAda(cn);
      PreparedStatement ps = cn.prepareStatement("INSERT INTO namatabel VALUES (?)");
      ps.setString(1, nilai);
      ps.executeUpdate();
    } catch (Exception ex) {
      System.out.println("Kesalahan [" + ex.getMessage() + "]");
      return false;
    }
    return true;
  }
}

Pada kode program di atas, saya mengandaikan bahwa nama user MySQL adalah jocki dan password-nya adalah password.  Bila di database tidak ada user lain, bisa juga menggunakan nama user root dan password dikosongkan (“” / string kosong).   Saya juga mengandaikan nama database yang diakses adalah latihan (ini harus sesuai dengan nama database yang ada di MySQL).

Jalankan web services dengan men-klik kanan pada nama proyek LatihanWebService kemudian memilih menu Run.  Jangan matikan server ini karena nanti akan dipanggil oleh aplikasi JME.

Untuk informasi lebih lanjut mengenai bagaimana cara membuat web services atau ingin membuat implementasi web services dalam bahasa lain, baca artikel yang saya tulis di https://thesolidsnake.wordpress.com/2012/07/04/membuat-web-service-soap-dengan-php-dan-memanggilnya-di-client-java/ , https://thesolidsnake.wordpress.com/2012/05/26/memanggil-web-service-dari-server-java-ee-di-client-php/, atau https://thesolidsnake.wordpress.com/2012/07/08/perbandingan-web-service-soap-antara-java-ee-vs-php/ .

Membuat Aplikasi JME Sebagai Client

Saya akan membuat sebuah aplikasi JME dengan memilih menu File, New Project…  Lalu memilih Java ME, Mobile Application.  Pada Project Name, saya memberi nama LatihanClient.  Saya menghilangkan tanda centang di bagian Create Hello Midlet.  Kemudian saya men-klik tombol Next.  Pada Emulator Platform, saya akan memiliki CLDC Oracle Java(TM) Platform Micro Edition SDK 3.0.5.  Lalu pada Device, saya memilih DefaultCldcMsaPhone1.   Saya memastikan Device Configuration bernilai CLDC-1.1 dan Device Profile bernilai MIDP-2.1.  Kemudian saya men-klik Finish untuk membuat proyek tersebut.

Saya membuat sebuah MIDlet baru dengan men-klik kanan pada nama proyek, kemudian memilih New, MIDlet…  Pada MIDlet Name, saya mengisi LatihanMidlet.  Pada Package, saya mengisi co.id.jocki.midlet.  Lalu saya men-klik tombol Finish.

Langkah berikutnya adalah membuat stub untuk web services.  Saya mulai dengan men-klik kanan pada nama proyek, memilih New, Others…  Pada MIDP, saya memilih Java ME Web Service Client dan men-klik tombol Next. Pada langkah Java ME Web Service Client Information, pilih Running Web Service,  isi WSDL URL dengan http://localhost:8080/LatihanWebService/AksesDatabase.    Ini adalah URL web service yang telah saya buat sebelumnya.  Lalu klik tombol Retrieve WSDL.  Jika tidak terjadi kesalahan, NetBeans akan mengisi field yang ada secara otomatis seperti yang terlihat pada gambar berikut ini:

Menambah Stub WS ke Java ME

Menambah Stub WS ke Java ME

Klik tombol Finish untuk selesai.

Sekarang, di proyek JME LatihanClient, akan ada package AksesDatabase yang dibuat secara otomatis.  Yang perlu saya lakukan sekarang adalah menambahkan kode program pada LatihanMiddlet.java yang akan memakai stub tersebut untuk mengakses web services.

Berikut ini adalah isi LatihanMiddlet.javayang telah saya modifikasi:

package co.id.jocki.midlet;

import aksesdatabase.AksesDatabase;
import aksesdatabase.AksesDatabase_Stub;
import java.rmi.RemoteException;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;

public class LatihanMidlet extends MIDlet {

    private List listData;
    private Form frmTambah;
    private TextField txtData;
    private Command cmdSimpan;
    private Command cmdTambah;

    public void startApp() {
        if (listData==null) {            
            listData = new List("Data Di Database", List.EXCLUSIVE);            
            cmdTambah = new Command("Tambah", Command.ITEM, 0);            
            listData.addCommand(cmdTambah);
            listData.setCommandListener(new CommandListener() {

                public void commandAction(Command c, Displayable d) {
                    if (c==cmdTambah) {
                        Display.getDisplay(LatihanMidlet.this).setCurrent(frmTambah);
                    }
                }

            });

            frmTambah = new Form("Akses Database");
            txtData = new TextField("Data", null, 20, TextField.ANY);
            frmTambah.append(txtData);
            cmdSimpan = new Command("Simpan", Command.OK, 0);
            frmTambah.addCommand(cmdSimpan);
            frmTambah.setCommandListener(new CommandListener() {

                public void commandAction(Command c, Displayable d) {
                    if (c==cmdSimpan) {
                        String data = txtData.getString();
                        simpanDataKeDatabase(data);
                        getDataDariDatabase();
                        Display.getDisplay(LatihanMidlet.this).setCurrent(listData);
                    }
                }

            });

        }
        getDataDariDatabase();
        Display.getDisplay(this).setCurrent(listData);
    }        

    private void getDataDariDatabase() {
        AksesDatabase aksesDatabase = new AksesDatabase_Stub();
        listData.deleteAll();
        try {
            String[] data = aksesDatabase.lihatIsiTabel();
            for (int i=0; i<data.length; i++) {
                listData.append(data[i], null);
            }
        } catch (RemoteException ex) {            
            listData.append("Kesalahan Akses Data [" + ex.getMessage() + "]", null);
        }
    }

    private void simpanDataKeDatabase(String data) {
        AksesDatabase aksesDatabase = new AksesDatabase_Stub();
        try {
            aksesDatabase.tambah(data);
        } catch (RemoteException ex) {
            listData.append("Kesalahan Simpan Data [" + ex.getMessage() + "]", null);
        }
    }

    public void pauseApp() {
    }

    public void destroyApp(boolean unconditional) {
    }
}

Sekarang, saya akan mencoba menjalankan aplikasi JME tersebut dengan men-klik icon Run (F6).  Tampilan awal adalah sebuah List yang harusnya berisi isi tabel (saat ini masih kosong karena belum ada data):

Tampilan Awal (Isi Tabel Masih Kosong)

Tampilan Awal (Isi Tabel Masih Kosong)

Pilih menu “Tambah” (dengan left soft key/tombol untuk menu kiri).  Akan muncul frmTambah yang berisi sebuah TextField.  Saya mengisi TextField ini dengan jocki, kemudian memilih menuSimpan:

Menambah Data Baru

Menambah Data Baru

Hal ini akan menyebabkan nilai “jocki” disimpan di tabel database.   Sekarang saat List mengambil data dari tabel di database, ia akan menemukan sebuah data:

Data Diambil Dari Database

Data Diambil Dari Database

Seandainya saya menambah sebuah data baru dengan nilai “keren”, isi List akan terlihat seperti:

Mengambil Data Dari Database

Mengambil Data Dari Database

Bila ingin lebih yakin lagi, silahkan periksa isi tabel di database dan pastikan terdapat dua record dengan nilai “jocki” dan “keren”.

Perbandingan Web Service SOAP antara Java EE vs PHP

Pada artikel Membuat Web Service SOAP dengan PHP Dan Memanggilnya Di Client Java, saya membuat web service dengan menggunakan PHP di  Zend Server Developer Edition.

Pada artikel Memanggil web service dari server Java EE di client PHP, saya membuat web service dengan menggunakan Java EE di GlashFish.

Kedua artikel tersebut membuat sebuah operasi yang sama sederhananya, yaitu operasi penjumlahan.   Hal ini menimbulkan sebuah rasa penasaran, mana yang lebih ‘baik‘ diantara keduanya?  Pengertian lebih ‘baik‘ disini bisa menjadi rancu bila tidak didefinisikan dengan jelas, apakah mengacu pada kecepatan, konsumsi sumber daya, stabilitas, kemudahan pengembangan atau yang lainnya?  Selain itu, walaupun terlihat seperti membandingkan Java dan PHP, tetapi banyak faktor lain yang terlibat, seperti sumber daya (CPU & memori) di komputer pengujian dan server (web server & application server) yang dipakai.   Misalnya, pada server produksi, banyak yang memakai PHP bersama dengan server NGIX yang cepat, sementara pada dunia akademis & pembelajaran, banyak yang lebih suka memakai PHP bersamaan dengan Apache HTTP Server (XAMPP, misalnya).

Untuk melakukan perbandingan, saya akan memakai Apache JMeter (http://jmeter.apache.org/).  Test plan yang saya buat dapat dilihat pada gambar berikut ini:

Test Plan JMeter

Test Plan JMeter

Pada konfigurasi tersebut saya membuat dua thread group yang berbeda, untuk pengujian PHP dan untuk pengujian Java.  Setiap thread group memiliki sebuah sampler yang sama yaitu WebService (SOAP) Request.  Walaupun mengakses server yang berbeda, kedua sampler tersebut akan mengirimkan SOAP Request yang sama, yaitu:

<?xml version="1.0" encoding="UTF-8"?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
  <S:Header/>
  <S:Body>
    <ns2:tambah xmlns:ns2="http://webservice.jocki.id.co/">
      <angka1>12</angka1>
      <angka2>12</angka2>
    </ns2:tambah>
  </S:Body>
</S:Envelope>

Agar tidak membebani kedua server, saya menambahkan sebuah constant timer dengan delay sebesar 100 ms pada setiap thread group, sehingga akan ada delay sebelum setiap request dikerjakan.

Saya akan  mulai dengan mensimulasikan 1 user yang melakukan 500 kali setiap request di setiap thread group.  Agar lebih akurat, saya mencoba pengujian yang sama sebanyak tiga kali.  Hasilnya adalah:

1 user thread 500 request – Pengujian 1
Label #Samples Average Min Max Std.Dev. Throughput KB/sec
WebService Java Request 500 5 3 742 33,16 9,0/sec 0,77
WebService PHP Request 500 34 25 2288 101,05 7,5/sec 0,64

1 user thread 500 request – Pengujian 2
Label #Samples Average Min Max Std.Dev. Throughput KB/sec
WebService Java Request 500 3 2 10 0,71 9,2/sec 0,78
WebService PHP Request 500 27 25 60 4,54 7,9/sec 0,67

1 user thread 500 request – Pengujian 3
Label #Samples Average Min Max Std.Dev. Throughput KB/sec
WebService Java Request 500 3 2 8 0,61 9,2/sec 0,78
WebService PHP Request 500 26 24 58 4,56 8,0/sec 0,68

Hasil percobaan ini menunjukkan bahwa server GlassFish (Java)  memiliki kecepatan yang lebih stabil bila dibandingkan dengan Zend Server Developer Edition (PHP).  Hal ini bisa dilihat dari nilai standard deviation.  Semakin kecil nilainya menunjukkan bahwa variasi kecepatan semakin sedikit (sampel yang ada semakin mendekati nilai rata-rata/average, tidak banyak yang melenceng jauh).  Selain itu, GlassFish memiliki transaksi per detik (TPS) atau throughput yang lebih besar dibanding Zend Server Developer Edition.

Pada percobaan berikutnya, saya akan mencoba mensimulasikan 2 user  yang mencoba mengakses server secara hampir bersamaan (concurrent).  Hasil pengujian dapat dilihat di tabel berikut ini:

2 user thread 500 request – Pengujian 1
Label #Samples Average Min Max Std.Dev. Throughput KB/sec
WebService Java Request 1000 5 3 661 21,20 17,9/sec 1,52
WebService PHP Request 1000 43 25 6048 250,76 13,6/sec 1,16

2 user thread 500 request – Pengujian 2
Label #Samples Average Min Max Std.Dev. Throughput KB/sec
WebService Java Request 1000 3 2 27 2,53 17,9/sec 1,53
WebService PHP Request 1000 30 25 59 5,49 14,9/sec 1,27

2 user thread 500 request – Pengujian 3
Label #Samples Average Min Max Std.Dev. Throughput KB/sec
WebService Java Request 1000 3 2 29 2,69 17,9/sec 1,52
WebService PHP Request 1000 30 25 71 4,97 15,0/sec 1,27

Hasil ini tidak berbeda jauh dengan pengujian sebelumnya dimana GlassFish (Java) memiliki nilai stabilitas dan throughput yang lebih besar dibanding dengan Zend Server Developer Edition (PHP).

Membuat Web Service SOAP dengan PHP Dan Memanggilnya Di Client Java

Pada tulisan ini, saya akan mencoba membuat sebuah web service SOAP + WSDL dengan PHP. Lalu, untuk pengujian, saya akan mencoba mengakses service dari sebuah program Java SE.

Membuat Web Service Provider Dengan PHP

SoapServer adalah class bawaan dari PHP untuk membuat server SOAP 1.1 dan SOAP 1.2. Sayangnya, SoapServer tidak memiliki fitur untuk membuat file WSDL secara otomatis. Bila ingin web service menyediakan WSDL, saya harus membuat file tersebut secara manual?!! Lalu bila pada perubahan definisi service, saya harus merombak file WSDL secara manual?!!

Itu sebabnya, kali ini saya akan framework PHP, yaitu Zend Framework. Framework andalan PHP ini menyediakan class Zend_Soap_Autodiscover untuk menghasilkan WSDL secara otomatis berdasarkan class/method yang akan dipublikasikan. Saya akan memakai IDE Zend Studio yang ‘sangat’ mendukung Zend Framework.

Saya mulai dengan membuat sebuah proyek PHP baru dengan nama server_ws dan lokasi penyimpanannya langsung di htdocs. Saya memakai Zend Framework 1.11.11. Untuk menghemat waktu, saya tidak akan membuat controller baru, melainkan langsung memodifikasi file application/controllers/IndexController.php.   Dan dengan alasan yang sama,  saya  menyertakan kode program class service langsung di file ini. Berikut ini adalah isi file IndexController.php yang telah saya modifikasi:

<?php
class Perhitungan {

  /**
   * Menghasilkan jumlah dari $angka1 dan $angka2
   *
   * @param int $angka1
   * @param int $angka2
   * @return int
   */
  function tambah($angka1, $angka2) {
    return $angka1 + $angka2;
  } 
}

class IndexController extends Zend_Controller_Action
{

  public function indexAction()
  {
    $this->getHelper('viewRenderer')->setNoRender(true);
    ini_set("soap.wsdl_cache_enabled", "0");

    if (isset($_GET['wsdl'])) {
      $autodiscover = new Zend_Soap_AutoDiscover();
      $autodiscover->setBindingStyle(array('style'=>'rpc'));
      $autodiscover->setOperationBodyStyle(array('use'=>'literal'));
      $autodiscover->setClass('Perhitungan');
      $autodiscover->handle();
    } else {
      $soap = new Zend_Soap_Server("http://localhost/server_ws/public/index?wsdl");
      $soap->setClass('Perhitungan');
      $soap->handle();
    }
  }
}
?>

Karena PHP tidak memiliki tipe data yang ketat, maka class yang akan dipanggil wajib memiliki docblock (@param dan @return). Nilai yang ada di docblock ini akan dipakai untuk melakukan pemetaan tipe data di WSDL nantinya. Pada contoh ini, saya membuat sebuah operasi sederhana yang hanya melakukan penjumlahan nilai parameter pertama dan nilai parameter kedua.

Pada IndexController, saya memanggil setNoRender(true) karena pada saat controller ini dipanggil, tidak akan ada view yang di-render, melainkan salah satu dari response SOAP atau WSDL yang akan dikirim ke client.

Bila client memanggil memanggil controller dengan menyertakan ?wsdl, maka IndexController akan membuat instance Zend_Soap_AutoDiscover yang akan menghasilkan WSDL. Pemanggilan method setClass dengan argumen Perhitungan menunjukkan bahwa WSDL akan dihasilkan berdasarkan isi class Perhitungan.

Penggunaan setBindingStyle dengan nilai array ‘style’=>’rpc’ menyebabkan WSDL yang dihasilkan akan memakai nilai “rpc” pada atribut “style” di <soap:binding>. Saat ini, bila memakai “document style“, web service tidak akan dapat dipanggil dengan baik di Java dan .NET (yang memakai standar WS-I). Permasalahan ini dapat dilihat di http://framework.zend.com/issues/browse/ZF-6351.

Bila controller dipanggil ini tanpa tambahan ?wsdl, maka sebuah instance Zend_Soap_Server akan dibuat dan siap untuk melayani SOAP request dari consumer.

Untuk menguji apakah WSDL berhasil di-generate secara otomatis, saya membuka browser dan mengetikkan alamat http://localhost/server_ws/public/index?wsdl. Seharusnya akan muncul WSDL dengan isi seperti berikut ini:

Tampilan WSDL Yang Dihasilkan Secara Otomatis

Tampilan WSDL Yang Dihasilkan Secara Otomatis

Membuat Web Service Consumer Dengan Java

Untuk memakai web service SOAP di Java, saya membuat sebuah proyek Java Application di NetBeans. Saya memberi nama proyek tersebut sebagai wsconsumer, dan membuat sebuah Main Class dengan nama co.id.jocki.Main.

Setelah proyek selesai dibuat, saya memilih menu File, New File. Pada dialog yang muncul, saya memilih Web Services di bagian Categories, dan memilih Web Service Client di bagian File Types seperti yang diperlihatkan di gambar berikut ini:

Membuat Web Service Client

Membuat Web Service Client

Setelah men-klik tombol Next, saya akan mengisi WSDL URL dengan lokasi URL yang mengembalikan WSDL, yaitu http://localhost/server_ws/public/index?wsdl. Saya mengisi bagian package dengan nilai co.id.jocki.webservice seperti yang diperlihatkan di gambar berikut ini:

Step Membuat Web Service Client

Step Membuat Web Service Client

Pada saya men-klik tombol Finish, NetBeans secara otomatis akan menjalan wsimport. Tools ini akan menghasilkan class Java secara otomatis sehingga saya tidak perlu membuat kode program yang rumit.

Untuk memanggil web service, saya perlu menambahkan beberapa baris perintah lagi. Tapi untungnya, NetBeans kembali membantu saya. Yang saya lakukan berikutnya adalah men-drag operasi yang akan dipanggil ke layar editor, seperti yang diperlihatkan di
gambar berikut ini:

Membuat Kode Program Secara Otomatis

Membuat Kode Program Secara Otomatis

Lalu saya menambahkan satu baris program pada method main seperti isi class Main adalah:

public class Main {

  public static void main(String[] args) {
    System.out.println("Hasil tambah(10,20) adalah " + tambah(10,20));
  }

  private static int tambah(int angka1, int angka2) {
    co.id.jocki.webservice.PerhitunganService service = new co.id.jocki.webservice.PerhitunganService();
    co.id.jocki.webservice.PerhitunganPort port = service.getPerhitunganPort();
    return port.tambah(angka1, angka2);
  }
}

Pada saat saya menjalankan program tersebut, ia akan mengirim SOAP REQUEST ke server PHP, menerima response dari server yang sama, dan akhirnya menampilkan output berupa:

Hasil tambah(10,20) adalah 30

Memakai MessageConverter di Spring 3.1 Untuk Web Services REST

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

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

Dependencies Maven

Dependencies Maven

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

package co.id.jocki.domain;

import java.io.Serializable;

public class Mahasiswa implements Serializable {

	private static final long serialVersionUID = 225855015823197676L;

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

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

}

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

package co.id.jocki;

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

import co.id.jocki.domain.Mahasiswa;

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

	private Mahasiswa mahasiswa;

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

}

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

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

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

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

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

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

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

</beans:beans>

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

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

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

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

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

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

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

</mapping>

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

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

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

Komunikasi REST dengan JSON

Komunikasi REST dengan JSON

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

Komunikasi REST dengan XML

Komunikasi REST dengan XML

Memanggil web service dari server Java EE di client PHP

Seorang mahasiswa yang sedang membuat skripsi bertanya apakah mungkin membuat sebuah web service dengan Java Enterprise Edition kemudian memanggilnya di client PHP?  Pada kesempatan tatap muka yang singkat, saya menjawab secara ringkas dengan merujuk pada definisi web services. Web services adalah sistem yang dirancang secara khusus untuk mendukung interaksi pertukaran data mesin ke mesin melalui jaringan. Selama berkomunikasi melalui metode yang sama, maka proses komunikasi dapat terjadi tanpa membedakan OS maupun bahasa pemograman yang dipakai.   Saat ini ada dua jenis web services, yaitu yang berbasis REST dan berbasis SOAP. Web services berbasis REST cenderung lebih ringan dan lebih mudah dipelajari dibandingkan dengan SOAP. Hal ini menyebabkan terjadinya peralihan dari web services berbasis SOAP ke REST.

Pada kesempatan tertulis ini, saya akan memberikan sebuah contoh halaman PHP yang memanggil web services yang dibuat dengan Java Enterprise Edition. Metode yang dipergunakan adalah metode berbasis SOAP.

Saya akan membuat dua proyek, yaitu:

  1. Server web service yang menggunakan teknologi Java Enterprise Edition. Saya menggunakan NetBeans IDE dan GlassFish bawaannya.
  2. Sebuah halaman PHP yang mengakses layanan web service yang disediakan oleh server di atas.

Membuat Server Web Service dengan Java EE

Langkah-langkah yang saya lakukan adalah:

  1. Memilih menu File, New Project di NetBeans IDE. Kemudian saya memilih Java Web di Categories, dan Web Application di Projects. Setelah memberi nama proyek dan menentukan lokasi penyimpanan, saya men-klik tombol Finish.
  2. Men-klik kanan nama proyek, kemudian memilih menu New, Other... Pada dialog yang muncul, saya memilih Web Services di bagian Categories, dan Web Service di bagian File Typesseperti yang terlihat di gambar berikut ini:

    Membuat Web Services Baru

    Membuat Web Services Baru

  3.  Pada Web Service Name, saya mengisi dengan nama PerhitunganWS. Pada bagian package, saya mengisi dengan nama package co.id.jocki.ws. Setelah itu, saya men-klik tombol Finish.

NetBeans akan membuat sebuah class baru yang berada di folder Web Services seperti yang terlihat pada gambar berikut ini:

Web Services Di Tampilan Project

Web Services Di Tampilan Project

Bila class PerhitunganWS di-buka, NetBeans memungkinkan pengguna untuk melihat dalam bentuk Source atau Design. Bila tampilan Design dipakai, maka layar editor akan terlihat seperti pada gambar berikut ini:

NetBeans Web Service Editor

NetBeans Web Service Editor

Untuk menambahkan sebuah operasi baru, saya melakukan langkah-langkah seperti berikut ini:

  1. Klik tombol Add Operation… Akan muncul sebuah dialog baru.
  2. Untuk menambah parameter, saya dapat men-klik tombol Add. Saya mengisi dialog tersebut seperti yang terlihat pada gambar berikut ini:

    Menambah Operasi Baru Di Web Service

    Menambah Operasi Baru Di Web Service

  3. Setelah itu, saya men-klik tombol OK.

Setelah ini, saya perlu menambahkan kode program yang berisi proses untuk operasi baru tersebut.   Saya men-klik Source di toolbar untuk beralih ke tampilan kode program.   Kemudian, saya mengubah satu-satunya baris di method tambah() menjadi return angka1 + angka2; seperti yang terlihat di gambar berikut ini:

Kode Program Operasi Di Web Service

Kode Program Operasi Di Web Service

Untuk menguji web service tersebut, klik kanan pada nama class PerhitunganWS, kemudian memilih Test Web Service seperti yang terlihat di gambar berikut ini:

Menguji Web Services

Menguji Web Service

NetBeans akan menjalankan browser yang berisi web service tester. Di halaman ini terdapat sebuah link bertuliskan WSDL File. URL disini nantinya akan dipakai oleh client web service. Di percobaan saya, nilai URL ini adalah http://localhost:8080/ServerWebService/PerhitunganWS?WSDL. Saya akan men-copy lokasi URL ini untuk dipakai di PHP nantinya.

Pada bagian methods, saya mencoba mengisi parameter dengan nilai 10 dan 20, kemudian setelah men-klik tombol tambah (nama method), akan muncul halaman tambah Method invocation. Pastikan pada halaman tersebut, terdapat tulisan Method returned int: “30”.

Sekarang server web service telah selesai dibuat. Saya tidak mematikan GlassFish server di NetBeans karena pada langkah berikutnya, saya akan memanggil web service ini di PHP.

Membuat Client Web Service dengan PHP

Sebelum mulai membuat client web service di PHP, saya memastikan apakah extension SOAP telah diaktifkan di PHP saya. Yang saya lakukan adalah membuat sebuah file PHP dengan nama info.php dimana isinya adalah <?php phpinfo(); ?>. Saat menampilkan halaman tersebut di browser, saya memastikan bahwa terdapat baris Soap Client dengan nilai Enabled.

Untuk membuat client web service, saya perlu membuat sebuah object dari class SoapClient seperti pada baris berikut ini:

$client = new SoapClient("http://localhost:8080/ServerWebService/PerhitunganWS?WSDL");

Nilai dari parameter constructur SoapClient adalah URL yang merujuk ke lokasi file WSDL. URL ini dapat dilihat di halaman web service tester pada saat saya menguji server web service di NetBeans.

Secara utuh, kode program PHP yang saya buat adalah:

<?php
function errorHandler($errno, $errstr, $errfile, $errline, array $errcontext) {
print "<h3>Terjadi kesalahan/peringatan:</h3>";
print "Baris $errline [$errstr]";
exit;
}

set_error_handler('errorHandler');

$client = new SoapClient("http://localhost:8080/ServerWebService/PerhitunganWS?WSDL");
$daftarOperasi = $client->__getFunctions();
print "<h3>Daftar Operasi Yang Tersedia Di Server WS:</h3>";
foreach ($daftarOperasi as $operasi) {
print "<p>$operasi</p>";
}
$hasil = $client->tambah(array('angka1'=>20, 'angka2'=>10));
print "<h3>Hasil operasi hitung(10,20): " . $hasil->return . "</h3>";
?>

<!--?php 

Pada kode program tersebut, saya menyertakan pemeriksaan kesalahan dengan set_error_handler(). Bagian tersebut dapat digantikan dengan menggunakan try/catch. Akan tetapi bila menggunakan try/catch, hanya pesan kesalahan yang ditampilkan, sementara pesan warning tidak akan ditampilkan. Bila pengaturan penampilan pesan kesalahan & warning secara otomatis tidak dimatikan (nilai display_errors di file konfigurasi adalah stdout), maka bagian ini tidak diperlukan.

Karena server web service di GlassFish menyediakan file WSDL, maka saya dapat menggunakan method __getFunctions() untuk melihat operasi apa saja yang disediakan oleh server web service tersebut.

Untuk memanggil salah satu operasi yang ada, saya cukup memanggil method dengan nama yang bersesuaian, diikuti dengan parameter yang diletakkan dalam associative array. Sebagai contoh, di kode program di atas, saya memanggil operasi tambah dengan nilai parameter angka1 berupa 20 dan nilai angka2 berupa 10. Hasil yang dikembalikan dari server web service adalah angka 30.