Thursday, June 30, 2011

Enums, PropertyChangeListener and State machine

State machines are key structures to build stateful applications over stateless messages. Following is a simple uni-directional state machine used as a compute engine with different processes triggering at different state of the machine. The machine has three states - Begin, Work and Finish.

When a Session is started the Machine sets itself in a Begin state, Each next() then switches the machine from the current state to the next. Processor(s) can be registered at different states of the machine.

The states are Begin -> Work -> Finish -> Null (Sink) and are defined via an Enum:
public enum State {
  BEGIN,
  WORK,
  FINISH;
}

Lets start with a Session (Also the state machine)

public class Session 
{
  private String BEGIN;
  private String WORK;
  private String END;

  /**
   * Create a Session state machine with three uni-directional states.
   * BEGIN -> WORK -> FINISH -> Null
   */
  private enum SessionState 
  {

    BEGIN() {
        @Override
        public SessionState next() {
            return WORK;
        }
    },

    WORK() {
        @Override
        public SessionState next() {
            return FINISH;
        }
    },

    FINISH() {
        @Override
        public SessionState next() {
            return null;
        }
    };

    public abstract SessionState next();

   }

private SessionState STATE = SessionState.BEGIN;

private String sessionId;

private PropertyChangeSupport propertyChangeSupport;

private Session() {
    propertyChangeSupport = new PropertyChangeSupport(this);
}

public static Session newSession() {
    return new Session();
}

void next() {
    synchronized (STATE) {
        SessionState old = STATE;
        STATE = STATE.next();
        if (STATE != null) {
            propertyChangeSupport.firePropertyChange(STATE.name(), old, STATE);
        }
    }
}

boolean isValid() {
    return STATE != null;
}

String sessionId() {
    switch (STATE) {
        case BEGIN:
            if (sessionId == null) {
                sessionId = UUID.randomUUID().toString();
            }
            return sessionId;
        case WORK:
            if (sessionId == null || sessionId.trim().equals("")) {
                throw new IllegalStateException("Session Id can not be null in WORK");
            }
            return sessionId;
        case FINISH:
            if (sessionId == null || sessionId.trim().equals("")) {
                throw new IllegalStateException("Session Id can not be null in  FINISH");
            }
            return sessionId;
        default:
            throw new IllegalStateException("Session has not yet started");
    }
}

void addStateChangeListener(State state, PropertyChangeListener listener) {
    propertyChangeSupport.addPropertyChangeListener(state.name(), listener);
}

}


Lets build some processing units - One for Work and the other for Finish states


public class WorkSessionListener implements PropertyChangeListener {
private final String sessionId;

public WorkSessionListener(String sessionId) {
    this.sessionId = sessionId;
}

 public void propertyChange(PropertyChangeEvent evt) {
    System.out.println ("[" + sessionId + " - Working]=" + evt.getOldValue() + " -> " + evt.getNewValue());
}
}

public class FinishSessionListener implements PropertyChangeListener {

 private final String sessionId;

public FinishSessionListener(String sessionId) {
    this.sessionId = sessionId;
}

public void propertyChange(PropertyChangeEvent evt) {
    System.out.println ("[" + sessionId + " - Finish]=" + evt.getOldValue() + " -> " + evt.getNewValue());
}
}


Now lets test it


import org.junit.Test;

public class SessionStateChangeTest {
@Test
public void testSessionStateChange() {
    final Session session = Session.newSession();
    final Session session2 = Session.newSession();

    session.addStateChangeListener(State.WORK, new WorkSessionListener(session.sessionId()));
    session.addStateChangeListener(State.FINISH, new FinishSessionListener(session.sessionId()));


    session2.addStateChangeListener(State.WORK, new WorkSessionListener(session2.sessionId()));

    Thread t1 = new Thread(new Runnable() {
        public void run() {
            while (session.isValid()) {
                session.next();
            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        public void run() {
            while (session2.isValid()) {
                session2.next();
            }
        }
    });

    t1.start();
    t2.start();

    try {
        t1.join();
        t2.join();
    } catch (InterruptedException exp) {

    }
}

}


We should see two BEGIN -> WORK and one WORK -> FINISH messages for two session ids.

Enjoy!

No comments: