Wednesday, October 28, 2015

Annotation processing during runtime

This post demonstrates how to process the annotation during runtime in order to add on additional functionalities to the target object without changing the object implementation.

The existing Account implementation:

Given the Account class below:

public interface IAccount {
    void withdraw();
    void deposit();
    void longProcess();
}

public class Account implements IAccount {
    
    public void withdraw() {
        System.out.println("Dummy method for withdraw..");
    }
    
    public void deposit() {
        System.out.println("Dummy method for deposit..");
    }
    
    public void longProcess() {
        System.out.println("Dummy method for longProcess..");
        try {
            Random random = new Random();
            int x = random.nextInt(4);
            Thread.sleep(x * 1000);
        } catch (InterruptedException ex) {
        }
    }
}

The method implementations are dummies. They are just pretending to carry on some specific accounting operations which we don't really care in this example. The more important part is how can we add additional functionality without changing the existing method implementation.

Requirement:

Capture the following information while an Account object is being accessed during runtime.
  • The invocation frequency of an accounting operation. By default, this data capturing is ON.
  • The longest processing time spent of an accounting operation. By default, this data capturing is OFF.

Solution:

Make use of Java Annotation and Java Dynamic Proxy.

Annotation interface:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Capture {
    public boolean invokeCount() default true;
    public boolean longestTimeSpent() default false;
}

First of all, we create an annotation interface called Capture. It has declared 2 annotation attributes to meet the requirement respectively. This annotation is only applied to Java methods. It's retention policy has to be RUNTIME so that, the annotated method is able to access the annotation information during runtime.

Apply annotation:

public class Account implements IAccount {
    
    @Capture
    public void withdraw() {
        System.out.println("Dummy method for withdraw..");
    }
    
    @Capture
    public void deposit() {
        System.out.println("Dummy method for deposit..");
    }
    
    @Capture(longestTimeSpent = true)
    public void longProcess() {
        System.out.println("Dummy method for longProcess..");
        try {
            Random random = new Random();
            int x = random.nextInt(4);
            Thread.sleep(x * 1000);
        } catch (InterruptedException ex) {
        }
    }
}

Account methods now are annotated with Capture annotation accordingly.

Invocation Handler:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CaptureInvocationHandler implements InvocationHandler {

    private final IAccount realAccount; // The real Account object
    private final Map<String, Integer> methodVisitFrequency;
    private final Map<String, Long> methodLongestTimeSpent;
    
    public CaptureInvocationHandler(IAccount account) {
        this.realAccount = account;
        this.methodVisitFrequency = new HashMap<>();
        this.methodLongestTimeSpent = new HashMap<>();
    }

    public Map<String, Integer> getMethodVisitFrequency() {
        return methodVisitFrequency;
    }

    public Map<String, Long> getMethodLongestTimeSpent() {
        return methodLongestTimeSpent;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method m = realAccount.getClass()
                .getMethod(method.getName(), method.getParameterTypes());
        if (m.isAnnotationPresent(Capture.class)) {
            String methodName = m.getName();
            
            // If invokeCount is ON
            if (m.getAnnotation(Capture.class).invokeCount()) {
                // Capture invocation frequency of the method.
                Integer frequency = methodVisitFrequency.get(methodName);
                if (frequency == null) {
                    methodVisitFrequency.put(methodName, 1);
                } else {
                    methodVisitFrequency.put(methodName, frequency + 1);
                }
            } 
            
            // If longestTimeSpent is ON
            if (m.getAnnotation(Capture.class).longestTimeSpent()) { 
                // Capture longest processing time spent of the method.
                long startTime = System.currentTimeMillis();
                Object returnObj = m.invoke(realAccount, args);
                long endTime = System.currentTimeMillis();
                long timeSpent = endTime - startTime;
                
                Long lastTimeSpent = methodLongestTimeSpent.get(methodName);
                if (lastTimeSpent == null) {
                    methodLongestTimeSpent.put(methodName, timeSpent);
                } else if (timeSpent > lastTimeSpent) {
                    methodLongestTimeSpent.put(methodName, timeSpent);
                }
                return returnObj;
            }
        }
        return m.invoke(realAccount, args);
    }
}

Invocation Handler is the bridge between the real object and the proxy object in Java Dynamic Proxy. CaptureInvocationHandler above is able to intercept the Account object method invocation and capture the desired data accordingly.

Application:

public static void main(String[] args) {

    // Invocation handler that keep the real Account object.
    CaptureInvocationHandler handler = new CaptureInvocationHandler(new Account());

    // Create a Account proxy object with CaptureInvocationHandler
    // as bridge to real Account object
    IAccount proxiedAcct
            = (IAccount) Proxy.newProxyInstance(Account.class.getClassLoader(),
                    new Class[]{IAccount.class}, handler);

    /*** Invoke Account operations - start ***/
    proxiedAcct.deposit();
    proxiedAcct.deposit();
    proxiedAcct.deposit();

    proxiedAcct.withdraw();
    proxiedAcct.withdraw();

    proxiedAcct.longProcess();
    proxiedAcct.longProcess();
    /*** Invoke Account operations - end ***/

    /*** Print captured data - start ***/
    for (Map.Entry<String, Integer> entry
            : handler.getMethodVisitFrequency().entrySet()) {
        System.out.println(entry.getKey() + " " + entry.getValue());
    }

    for (Map.Entry<String, Long> entry
            : handler.getMethodLongestTimeSpent().entrySet()) {
        System.out.println(entry.getKey() + " " + entry.getValue() + "ms");
    }
    /*** Print captured data - end ***/
}

Result:
Dummy method for deposit..
Dummy method for deposit..
Dummy method for deposit..
Dummy method for withdraw..
Dummy method for withdraw..
Dummy method for longProcess..
Dummy method for longProcess..
deposit 3
longProcess 2
withdraw 2
longProcess 2006ms

Summary:

As you can see, besides adding the annotation onto methods in Account class, there is no change in method implementation. The method access information capturing is done in Invocation Handler. If the Account proxy object creation is bothering you, then you actually could move the object creation into a Factory Method. If you still don't like the way Java Dynamic Proxy works, check out the CGLib in my other post - What makes the CGLib Proxy Enhancer the good substitution for the Java Dynamic Proxy.



Tuesday, October 13, 2015

How to create display message in correct form of singular and plural

Putting the message into resource bundle properties allows this message to be translated into other languages. This is not the end of the story because there are messages need to conform to the language's grammar. The most classic example is the singular and plural form in the English message as below:
There are no plugins available for installation.
There is one plugin available for installation.
There are 3 plugins available for installation.
The highlighted part is a numeric variant and the message change its form based on this variant.


Solution 1: Deficient

Bundle.properties:
none.msg = There are no plugins available for installation.
singular.msg = There is one plugin available for installation.
plural.msg = There are {0} plugins available for installation.
Code:
    private static ResourceBundle BUNDLE = ResourceBundle.getBundle("Bundle");

    private static void showMessage(int total) {
        if (total == 1) {
            System.out.println(BUNDLE.getString("singular.msg"));
        } else if (total > 1) {
            System.out.println(
                    MessageFormat.format(BUNDLE.getString("plural.msg"), total));
        } else {
            System.out.println(BUNDLE.getString("none.msg"));
        }
    }

    public static void main(String[] args) {
        showMessage(0);
        showMessage(1);
        showMessage(2);
    }

Result:
There are no plugins available for installation.
There is one plugin available for installation.
There are 2 plugins available for installation.
Summary:
Although the intention is good, but solution 1 is deficient because
  • The showMessage() implementation does more than just showing a message.
  • A message appears in 3 forms which most likely not true for non-English language. Translator has to provide the same message for 3 different keys in the Bundle.properties.


Solution 2: Not good enough

Bundle.properties:
msg = There is {0} plugin(s) available for installation.
Code:
    private static ResourceBundle BUNDLE = ResourceBundle.getBundle("Bundle");

    private static void showMessage(int total) {
        System.out.println(MessageFormat.format(BUNDLE.getString("msg"), total));
    }

    public static void main(String[] args) {
        showMessage(0);
        showMessage(1);
        showMessage(2);
    }

Result:
There is 0 plugin(s) available for installation.
There is 1 plugin(s) available for installation.
There is 2 plugin(s) available for installation..
Summary:
Solution 2 resolved the problems given by solution 1. However, it gives up the English grammar.


Solution 3: Good

Bundle.properties:
msg = There {0, choice, 0#are no plugins|1#is one plugin|1<are {0, number, integer} plugins} available for installation.
Code:
    private static ResourceBundle BUNDLE = ResourceBundle.getBundle("Bundle");

    private static void showMessage(int total) {
        System.out.println(MessageFormat.format(BUNDLE.getString("msg"), total));
    }

    public static void main(String[] args) {
        showMessage(0);
        showMessage(1);
        showMessage(2);
    }

Result:
There are no plugins available for installation.
There is one plugin available for installation.
There are 2 plugins available for installation.
Summary:
The message in the Bundle.properties is a java.text.ChoiceFormat pattern. It can be digested by MessageFormat. With a single message in Bundle.properties, solution 3 is able to meet the requirement perfectly.



References:
http://docs.oracle.com/javase/7/docs/api/java/text/MessageFormat.html
http://docs.oracle.com/javase/7/docs/api/java/text/ChoiceFormat.html

Saturday, October 10, 2015

How to internationalize a display message

The requirement is to display the following message from our Java program.
Your last transaction was on October 11, 2015 at 8:46:03 AM with transaction amount $100.00.
The message has to be able to support the localization process. That means the message could be translated and conform to the expectations of a given user community. The highlighted parts are variances of date, time, and currency that are supplied to the message.

Solution 1: Deficient

Bundle.properties:
msgPart1 = Your last transaction was on
msgPart2 = at
msgPart3 = with transaction amount
Code:
// Message stored as parts in Bundle.properties
ResourceBundle bundle = ResourceBundle.getBundle("Bundle");

// Retrieve message parts from resource bundle
String msgPart1 = bundle.getString("msgPart1");
String msgPart2 = bundle.getString("msgPart2");
String msgPart3 = bundle.getString("msgPart3");

// Variants
double trxnAmount = 100;
Date trxnDateTime = new Date();

// Applying formating based on user default locale
DateFormat shortDateFormat = SimpleDateFormat.getDateInstance(DateFormat.LONG);
DateFormat shortTimeFormat = SimpleDateFormat.getTimeInstance(DateFormat.MEDIUM);
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();

// Construct message
StringBuilder message = new StringBuilder();
message.append(msgPart1).append(' ').append(shortDateFormat.format(trxnDateTime)).append(' ')
        .append(msgPart2).append(' ').append(shortTimeFormat.format(trxnDateTime)).append(' ')
        .append(msgPart3).append(' ').append(currencyFormat.format(trxnAmount))
        .append('.');

// Print and display the message
System.out.println(message.toString());

Result:
Your last transaction was on October 11, 2015 at 11:54:24 AM with transaction amount $100.00.
Summary:
Solution 1 is deficient because
  • The message is split into a few parts and this make translation unfriendly.
  • All the message parts are required to be concatenated and it is awkward to append space and period to construct to the whole message.
  • Verbose line of code to construct the message.

Solution 2: Good

Bundle.properties:
msg = Your last transaction was on {0} at {1} with transaction amount {2}.
Code:
    // Message stored in Bundle.properties
    ResourceBundle bundle = ResourceBundle.getBundle("Bundle");

    // Retrieve message parts from resource bundle
    String msg = bundle.getString("msg");

    // Variants
    double trxnAmount = 100;
    Date trxnDateTime = new Date();

    // Applying formating based on user default locale
    DateFormat shortDateFormat = SimpleDateFormat.getDateInstance(DateFormat.LONG);
    DateFormat shortTimeFormat = SimpleDateFormat.getTimeInstance(DateFormat.MEDIUM);
    NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
    
    // Collect variants into array
    Object[] arguments = new String[] {
        shortDateFormat.format(trxnDateTime),
        shortTimeFormat.format(trxnDateTime),
        currencyFormat.format(trxnAmount)
    };
    
    // Print and display the message
    System.out.println(MessageFormat.format(msg, arguments));

Result:
Your last transaction was on October 11, 2015 at 11:54:24 AM with transaction amount $100.00.
Summary:
Solution 2 makes use of java.text.MessageFormat. This allows the message to includes the variants in it and hence is translation friendly.

Solution 3: Better

Bundle.properties:
msg = Your last transaction was on {0, date, long} at {0, time, medium} with transaction amount {1, number, currency}.
Explanation:
{0, date, long} : Use the Date Formatter to format the arguments[0] object and apply with DateFormat.LONG style.
{0, time, medium} : Use the Time Formatter to format the arguments[0] object and apply with DateFormat.MEDIUM style.
{0, number, currency} : Use the Currency Formatter to format the arguments[1].

Code:
    // Message stored in Bundle.properties
    ResourceBundle bundle = ResourceBundle.getBundle("Bundle");

    // Retrieve message parts from resource bundle
    String msg = bundle.getString("msg");

    // Variants
    double trxnAmount = 100;
    Date trxnDateTime = new Date();
    
    // Collect variants into array
    Object[] arguments = new Object[] {
        trxnDateTime,
        trxnAmount
    };
    
    // Print and display the message
    System.out.println(MessageFormat.format(msg, arguments));

Result:
Your last transaction was on October 11, 2015 at 11:54:24 AM with transaction amount $100.00.
Summary:
Solution 3 is even better because
  • The formatting jobs are not required in the code because they are delegated to java.text.MessageFormat.
  • The formatting style such as LONG or MEDIUM date could be configured in the properties file instead of hardcoded in the source code.

References: