Memakai Event Griffon Di simple-jpa


Salah satu penambahan yang saya lakukan di simple-jpa 0.5 adalah dukungan event Griffon.   Plugin simple-jpa kini akan menghasilkan event Griffon yang berupa:

  • simpleJpaCreateEntityManager – Event ini akan dihasilkan setiap kali simple-jpa membuat Entitymanager baru.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaDestroyEntityManagers – Event ini akan dihasilkan saat simple-jpa menutup seluruh EntityManager yang ada.
  • simpleJpaBeforeCloseEntityManager – Event ini akan dihasilkan pada saat simple-jpa menutup sebuah EntityManager.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaNewTransaction – Event ini akan dihasilkan setiap kali simple-jpa membuat transaksi baru.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaCommitTransaction – Event ini akan dihasilkan setiap kali sebuah transaction di-commit.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaRollbackTransaction – Event ini akan dihasilkan setiap kali sebuah transaction di-rollback.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaBeforeAutoCreateTransaction – Event ini akan dihasilkan bila simple-jpa secara otomatis membuat transaction baru bila sebuah operasi simple-jpa dikerjakan diluar transaksi.   Plugin simple-jpa akan selalu mengerjakan operasi JPA dalam sebuah transaksi; bila tidak ada transaksi yang aktif, maka ia akan membuat transaksi baru khusus untuk mengerjakan operasi tersebut.

Bagaimana cara memakai event tersebut? Pada file griffon-app/conf/Events.groovy, tambahkan kode program seperti berikut ini:

onSimpleJpaCreateEntityManager =  { TransactionHolder th ->
  // kode program yang akan dikerjakan saat sebuah EntityManager baru dibuat
  ...
}

onSimpleJpaNewTransaction = { TransactionHolder th ->
  // kode program yang akan dikerjakan saat transaksi baru dibuat
  ...
}

Salah satu contoh kegunaan dari event yang dihasilkan simple-jpa adalah untuk memberikan penanda bahwa aplikasi sedang sibuk.   Saya menemukan bahwa terkadang sebuah transaksi JPA bisa berlangsung lebih dari beberapa detik terutama bila transaksi tersebut melibatkan banyak object (misalnya dalam membuat laporan).   Agar pengguna tidak bingung dengan respon yang lama, akan lebih baik bila aplikasi menampilkan indikator bahwa sebuah transaksi sedang berlangsung.  Untuk menampilkan indikator sibuk, saya akan menggunakan plugin jxlayer yang akan memanfaatkan JLayer di Java 7.   Saya dapat men-install plugin tersebut dengan memberikan perintah:

griffon install-plugin jxlayer

JLayer bekerja dengan memanfaatkan decorator design pattern.   Saya tidak perlu menambahkan indikator sibuk tersebut pada view yang sebelumnya sudah dibuat.   Saya hanya perlu membuat sebuah dekorator yang nantinya akan diterapkan pada satu atau lebih view.   Sebagai contoh, saya akan membuat class Java (jangan lupa bahwa Groovy tetap bisa memakai class Java) bernama BusyLayerUI yang isinya seperti berikut ini:

import griffon.core.UIThreadManager;
import javax.swing.*;
import javax.swing.plaf.LayerUI;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.geom.Arc2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;

class BusyLayerUI extends LayerUI<JPanel> implements ActionListener {

    public static final BusyLayerUI instance = new BusyLayerUI();
    public static BusyLayerUI getInstance() {
        return instance;
    }

    private final int RADIUS = 150;
    private final Color WARNA_LATAR = new Color(219, 247, 186);
    private final Color WARNA_PROGRESS = new Color(118, 186, 39);
    private final Color WARNA_BAYANGAN = new Color(150, 240, 82, 157);

    private boolean visible = false;
    private BufferedImage pixelTexture;
    private Rectangle2D ukuranTexture;
    private Arc2D.Double fullCircle, progress;
    private Timer timer;

    private BusyLayerUI() {
        pixelTexture = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
        ukuranTexture = new Rectangle2D.Double(0, 0, 2, 2);
        Graphics2D g2 = pixelTexture.createGraphics();
        g2.setColor(Color.BLACK);
        g2.fillRect(0, 0, 1, 2);
        g2.fill(ukuranTexture);
        g2.setColor(Color.GRAY);
        g2.fillRect(1, 0, 1, 2);
        g2.dispose();
        pixelTexture.flush();

        progress = new Arc2D.Double(50, 50, 400, 400, 0, 0, Arc2D.OPEN);
        fullCircle = new Arc2D.Double(50, 50, 400, 400, 0, -360, Arc2D.OPEN);

    }

    void show() {
        if (visible) return;
        UIThreadManager.getInstance().executeSync(new Runnable() {
            public void run() {
                progress.setAngleExtent(0);
                timer = new Timer(1000/24, BusyLayerUI.this);
                timer.start();
                visible = true;
                firePropertyChange("visible", false, true);
            }
        });
    }

    void hide() {
        if (!visible) return;
        UIThreadManager.getInstance().executeSync(new Runnable() {
            public void run() {
                visible = false;
                firePropertyChange("visible", true, false);
            }
        });
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        super.paint(g, c);
        if (!visible) return;

        int w = c.getWidth();
        int h = c.getHeight();

        Graphics2D g2 = (Graphics2D) g.create();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        Composite currentComposite = g2.getComposite();

        // Buat layar terlihat seperti tidak aktif (lebih gelap dan kabur)
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
        g2.setPaint(new TexturePaint(pixelTexture, ukuranTexture));
        g2.fillRect(0, 0, w, h);

        double centerX = (double) (w/2);
        double centerY = (double) (h/2);

        // Buat lingkaran terang
        g2.setStroke(new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g2.setColor(WARNA_LATAR);
        fullCircle.setFrameFromCenter(centerX, centerY, centerX+RADIUS, centerY+RADIUS);
        g2.draw(fullCircle);

        // Buat progress yang menandakan program sedang sibuk
        g2.setColor(WARNA_PROGRESS);
        progress.setFrameFromCenter(centerX, centerY, centerX+RADIUS, centerY+RADIUS);
        g2.draw(progress);
        g2.setColor(WARNA_BAYANGAN);
        g2.setStroke(new BasicStroke(30, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g2.draw(progress);

        // Selesai
        g2.setComposite(currentComposite);
        g2.dispose();
    }

    @Override
    public void applyPropertyChange(PropertyChangeEvent evt, JLayer<? extends JPanel> l) {
        if ("tick".equals(evt.getPropertyName()) || "visible".equals(evt.getPropertyName())) {
            l.repaint();
        }
    }

    @Override
    public void installUI(JComponent c) {
        super.installUI(c);
        ((JLayer)c).setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK);
    }

    @Override
    public void uninstallUI(JComponent c) {
        ((JLayer)c).setLayerEventMask(0);
        super.uninstallUI(c);
    }

    @Override
    public void eventDispatched(AWTEvent e, JLayer<? extends JPanel> l) {
        if (visible && e instanceof InputEvent) {
            ((InputEvent)e).consume();
        }
    }

    public void actionPerformed(ActionEvent e) {
        if (visible) {
            double extend = progress.getAngleExtent();
            if (extend <= -360) {
                extend = 0;
            } else {
                extend -= 3;
            }
            progress.setAngleExtent(extend);
            firePropertyChange("tick", 0, 1);
        } else {
            timer.stop();
        }
    }
}

Bila method show() dipanggil, maka sebuah animasi lingkaran akan ditampilkan dan seluruh komponen di layar tersebut menjadi tidak dapat di-klik atau di-isi.   Pengguna hanya boleh pasrah menunggu🙂

Sekarang saya perlu menentukan view apa saja yang perlu memiliki ‘informasi sibuk’ ini.   Sebagai contoh, saya dapat mendekorasi view utama dengan mengubah file MainGroupView.groovy menjadi seperti berikut ini:

application(...){

  ...
  borderLayout()
  jxlayer(UI: BusyLayerUI.getInstance(), constraints: BorderLayout.CENTER) {

    panel() {
      borderLayout()

       toolBar(id: 'toolBar', constraints: BorderLayout.PAGE_START, floatable: false) {
         ...
       }

       panel(id: "mainPanel") {
         cardLayout(id: "cardLayout")
       }

       statusBar(constraints: BorderLayout.PAGE_END, border: BorderFactory.createBevelBorder(BevelBorder.LOWERED)) {
         ...
       }
    }
  }
}

Sekarang pertanyaannya adalah bagaimana mengetahui sebuah transaksi sedang berlangsung sehingga method show() akan dipanggil?    Apakah saya harus mengubah kode program di controller dimana saya selalu memanggil show() sebelum memulai transaksi dan memanggil hide() setelah transaksi selesai dikerjakan?   Bila demikian, banyak sekali perubahannya karena hampir seluruh method harus diubah!   Tapi karena simple-jpa 0.5 sudah mempublikasi event yang berkaitan dengan transaksi, maka saya bisa dengan mudah mengetahui kapan sebuah transaksi dimulai dan diakhiri (berlaku secara global).   Dengan demikian, saya hanya perlu menambahkan kode program berikut ini pada griffon-app/conf/Events.groovy:

import simplejpa.transaction.TransactionHolder
import util.BusyLayerUI

onUncaughtExceptionThrown = { Exception e ->
    if (e instanceof org.codehaus.groovy.runtime.InvokerInvocationException) e = e.cause.cause
    javax.swing.JOptionPane.showMessageDialog(null, e.message, "Error", javax.swing.JOptionPane.ERROR_MESSAGE)
    BusyLayerUI.getInstance().hide()
}

onSimpleJpaNewTransaction = { TransactionHolder th ->
    BusyLayerUI.getInstance().show()
}

onSimpleJpaCommitTransaction = { TransactionHolder th ->
    BusyLayerUI.getInstance().hide()
}

onSimpleJpaRollbackTransaction = { TransactionHolder th ->
    BusyLayerUI.getInstance().hide()
}

Sekarang, setiap kali sebuah transaksi sedang berlangsung, layar utama akan menampilkan indikator sibuk seperti yang terlihat pada gambar berikut ini:

Indikator sibuk setiap kali transaksi berlangsung

Indikator sibuk setiap kali transaksi berlangsung

Sebagai informasi tambahan, JPA sendiri sudah memiliki mekanisme event yang disebut sebagai entity listener.   Hasil scaffolding dari simple-jpa sudah mendaftarkan sebuah entity listener bernama simplejpa.AuditingEntityListener seperti yang terlihat pada gambar berikut ini:

Isi file orm.xml

Isi file orm.xml

Pengguna dapat menambahkan entity listener baru pada file orm.xml tersebut.   Method pada entity listener dapat memiliki annotation yang mewakili lifecycle event JPA, yaitu @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove dan @PostLoad.   Berbeda dengan event dari simple-jpa yang berkaitan dengan transaksi, event disini lebih berkaitan dengan siklus hidup masing-masing object yang dikelola oleh JPA.

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: