AOP and Swing : a smart association

le 14/04/2010 par Olivier Mallassi
Tags: Software Engineering

This is not a scoop : Swing - even if this technology is widely used in companies - is not evolving a lot. The developer kit still provides today components which are neither complex nor rich as a couple of years ago so you have to buy it elsewhere. The Swing development is still very verbose and finally not really productive, and to be honest, it is not the few JSR in stand-by that will change anything. Beans Binding is in an inactive status. Beans Validation - the draft dates from 2008 - is always not included into the JDK (maybe the version 7). The JSR 296 (which defined a standard application lifecycle) will not represent the most important improvement a framework has known (I swear, I really enjoy Swing...) So we have to deal with all these misses. Moreover, I am fond of AOP since the first version of AspectJ came out. I really think this is an elegant way of developing which is today well equipped (IDE integration, Maven integration...) to be safely used.

So here are a few recurrent Swing concerns that AOP can help to resolve.

Service injection

The idea is quite simple : implementing the IOC pattern by injecting a service implementation (for instance a Spring bean) into all properties annotated with the custom @Inject annotation.

For instance, injecting the service myService in the following action (the code can be improved but in this example, the bean is injected using the property name and not the property type) :

public class MyActionListener implements ActionListener{
@Inject
private MyService myService

...
}

The following aspect enables injection of a service called myService

public aspect SpringInjectAspect {
    pointcut fieldsInjection() : within(com.mypackage..*) 
     && execution(public void java.awt.event.ActionListener+.actionPerformed(..));

    before() : fieldsInjection() {
        Class currentClass = thisJoinPoint.getThis().getClass();
        Object currentObject = thisJoinPoint.getThis();
        //to inject bean spring even in super class property
        while (!currentClass.equals(ActionListener.class)) {
             injectBean(currentClass, currentObject);
             //for te next step
             currentClass = currentClass.getSuperclass();
        }
    }

    private void injectBean(Class currentClass, Object currentObject) {
        Field[] fields = currentClass.getDeclaredFields();

        for (int i = 0; i < fields.length; i++) {
            Annotation associatedAnnotation = fields[i].getAnnotation(Inject.class);
            if (associatedAnnotation != null) {
                try {
                    //get the bean by the field name
                    Object implementationObject = SpringHelper.getApplicationContext().getBean(
                            fields[i].getName());
                    try {
                        fields[i].setAccessible(true);
                        fields[i].set(currentObject, implementationObject);
                    }
                    catch (IllegalArgumentException e) {
                       ... do what you need to do
                }
                catch (NoSuchBeanDefinitionException e) {
                //log the fact the asked bean do not exist
                    throw e;
                }
            }
        }
    }
}

Tooling of the binding mechanism

Using a binding mechanism like jGoodies can be sometimes interesting but often implies to write additional and worthless code like :

public void setBooleanValue(boolean newValue) {
  boolean oldValue = booleanValue;
  booleanValue = newValue;
  changeSupport.firePropertyChange("booleanValue", oldValue, newValue); 
 }

In this example, each setter must be enhanced with a call to the firePropertyChange method which precises the new and old values and the property name (which will be error prone since the code will be copied and pasted in each setter...)

Here again, AOP can help by providing us a way of enhancing a "classical" setter (ie. that only set the p property). What I call a classical setter is :

public void setBooleanValue(boolean newValue) {
  booleanValue = newValue;
 }

and the following aspect do the rest of the job

public aspect jGoodiesBindingAspect {
    pointcut propertyEnhancement() : within(com.mypackage..*) 
         && execution (public void com.jgoodies.binding.beans.Model+.set*(..))

    void around() : propertyEnhancement() {
        Object[] args = thisJoinPoint.getArgs();
        String propertyName = thisJoinPoint.getSignature().getName().substring(3);

        Object oldValue = null;
        try {
            Method propertyGetter = thisJoinPoint.getThis().getClass()
                    .getMethod("get" + propertyName);
            oldValue = propertyGetter.invoke(thisJoinPoint.getThis());
            Object newValue = args[0];
            
            proceed();
            // if no error occurs
            Method firePropertyChangeMethod = com.jgoodies.binding.beans.Model.class.getDeclaredMethod(
                    "firePropertyChange", String.class, Object.class, Object.class);
            firePropertyChangeMethod.setAccessible(true);
            propertyName = org.apache.commons.lang.StringUtils.uncapitalize(propertyName);
            firePropertyChangeMethod.invoke(thisJoinPoint.getThis(), propertyName, oldValue, newValue);
        }
        catch (NoSuchMethodException 
            ...do what you need to do
    }
}

In that case, the pointcut weaves all the methods, from any classes inheriting com.jgoodies....Model, that start with set.

GUI management authorizations

Here again this is a really standard concern: being sure that the functions (I mean buttons, menus...) and datas are not accessible, alterable in the Swing GUI. So we are simply talking about enabling or disabling the widgets that need to be (ie. myComponent.setEnabled(...)).

The first step is to define an annotation that will permit to define the roles on a JComponent. In the following sample, the widget myComboBox is enabled only for the users that have the roles "consultation-level-1" and "consultation-level-2".

@Authorize(roles="consultation-level-1, consultation-level-2"
private JComponent myComboBox;

So, the following aspect will enable or disable the widgets based on the previously defined roles

public aspect AuthorizationAspect {
    public pointcut authorized_gui() :  within(com.mypackage..*) 
        && set(@Authorize * *..*.*);

    after() : authorized_gui() {
        String pointCutKind = thisJoinPoint.getKind();
        if (JoinPoint.FIELD_SET.equals(pointCutKind)) {
            java.lang.reflect.Field field = ((FieldSignature) thisJoinPoint.getSignature()).getField();
            try {
                field.setAccessible(true);
                Object theField = field.get(thisJoinPoint.getThis());
                if (theField instanceof JComponent) {
                    Authorize authorization = field.getAnnotation(Authorize.class);
                    String[] roles = authorization.roles();
                    //ask your security context holder the role the user have
                    ((JComponent) theField).setEnabled(SecurityContextHolder.getInstance()
                            .isUserInRole(roles));
                }
            }
            catch (IllegalAccessException e) {
                throw new TechnicalException(e);
            }
        }
    }
}

The tricky point in the aspect is the set(@Authorize * *..*.*) that weaves into the component instantiation (and not the call to the constructor) of all the properties that have the specified annotation.

So here are a couple of concerns AOP helped us to solve in an elegant, simple and non intrusive way. Other AOP usages exist. The most obvious one is to automatically manage the runtime exceptions. In a GUI context, these exceptions will usually be displayed - sometimes in a popup - with a message like "an error occurs, please contact your administrator". Other usage can be in validating some specific coding rules (using “declare warning” and “declare error”).

Do you have other GUI concerns you have been able to manage this way?