Hi all,
As default behavior, SWF stores conversation data in session and there could exist situations where it is needed to persist that conversation data in a non volatile data warehouse. For instance, in my case was required to "pause" a flow on determined states allowing the user to restore the flow exactly in the same point the user stopped his work. Initially, achieving that was a big challenge for me because there is not too much information about how to do that and after several weeks googling, reading and developing I have my flows working with persisted conversation data.
Well, so I have decided to post the solution for those who have similar requirements when using SWF. My solution works with conversation data persisted on DB (MySQL) and is based on the solution approached in the book The Definitive Guide to Spring Web Flow (http://www.apress.com/9781430216247/) by Erwin Vervaet, creator and one of the fathers of Spring Webflow. The original solution is useful if you are working with SWF 1 so I have adapted that for working on SWF2. At the same time, I am posting it with the intention of receiving comments and possible improvements for both the code and the implementation because I am sure that it is possible to make it much better!
Basically it is required to implement three classes:
- PersistentConversation: replacement for the original ContainedConversation SWF2's implementation.
- PersistentConversationManager: replacement for the original SessionBindingConversationManager SWF2's implementation.
- PersistentConversationHolder: replacement for the original ConversationContainer SWF2's implementation
Aditionally, I also needed a PersistentConversationHelper for unify business logic invocation and a JdkConcurrentConversationLock copied from SWF1 source code (I know it is very tricky but I need to solve the problem ASAP, any alternative?). At the same time, I have a business logic and DAO classes for CRUD operations which I will not post because I think its too obvious (but if somebody requires it, feel free to ask for it).
And now, here you go the code (original code of Erwin Vervaet still remains but commented):
- PersistentConversation
Code:public class PersistentConversation implements Conversation, Serializable { /** * */ private static final long serialVersionUID = -3410255383231814217L; private ConversationId id; private Map<Object, Object> attributes; // private transient ConversationDao dao; private ConversationLock lock = new JdkConcurrentConversationLock(); private int lockCount = 0; private boolean ended = false; // protected PersistentConversation() { // } // public PersistentConversation(ConversationId id, ConversationDao dao) { // this.id = id; // this.attributes = new HashMap<Object, Object>(); // this.dao = dao; // } /** * Constructor to be used for instancing a new PersistentConversation to be persisted. * * @param id */ public PersistentConversation(ConversationId id, String flowName { this.id = id; this.attributes = new HashMap<Object, Object>(); this.attributes.put("name", "flows/green-mode-method"); } /** * Constructor to be used for instancing a PersistentConversation already persisted. * * @param webFlowConversation */ public PersistentConversation(WebFlowConversation webFlowConversation) { setIdAsString(webFlowConversation.getId()); setAttributesAsByteArray(webFlowConversation.getAttributes()); } public ConversationId getId() { return id; } protected void setId(ConversationId id) { this.id = id; } public void lock() { lock.lock(); lockCount++; } public Object getAttribute(Object name) { return attributes.get(name); } public void putAttribute(Object name, Object value) { attributes.put(name, value); } public void removeAttribute(Object name) { attributes.remove(name); } public void end() { // dao.deleteConversation(id); PersistentConversationHelper.deleteConversation(id); ended = true; } public void unlock() { lockCount--; lock.unlock(); if (lockCount == 0) { if (!ended) { // dao.updateConversation(this); PersistentConversationHelper.updateConversation(this); } PersistentConversationHolder.removeConversation(id); } } @Override public String toString() { return new ToStringCreator(this).append("id", id).append("attributes", attributes).toString(); } // persistence helpers // public void setDao(ConversationDao dao) { // this.dao = dao; // } protected String getIdAsString() { return getId().toString(); } protected void setIdAsString(String id) { setId(new SimpleConversationId(id)); } protected byte[] getAttributesAsByteArray() { try { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(bout); oout.writeObject(attributes); oout.flush(); return bout.toByteArray(); } catch (Exception e) { // should not happen throw new RuntimeException("Exception serializing attributes map", e); } } @SuppressWarnings("unchecked") protected void setAttributesAsByteArray(byte[] bytes) { try { this.attributes = (Map<Object, Object>) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject(); } catch (Exception e) { // should not happen throw new RuntimeException("Exception deserializing attributes map", e); } } }- PersistentConversationManager
Code:@Component(value = "persistentConversationManager") public class PersistentConversationManager implements ConversationManager { // private UidGenerator conversationIdGenerator = new RandomGuidUidGenerator(); // private ConversationDao conversationDao; // public void setConversationDao(ConversationDao conversationDao) { // this.conversationDao = conversationDao; // } public Conversation beginConversation(ConversationParameters conversationParameters) throws ConversationException { // ConversationId convId = new SimpleConversationId(conversationIdGenerator.generateUid()); ConversationId convId = new SimpleConversationId(UUID.randomUUID()); // conversationDao.createConversation(new PersistentConversation(convId, conversationDao)); PersistentConversationHelper.createConversation(new PersistentConversation(convId, conversationParameters.getName())); return getConversation(convId); } public Conversation getConversation(ConversationId id) throws ConversationException { if (PersistentConversationHolder.holdsConversation(id)) { // we already loaded the conversation for the calling thread return PersistentConversationHolder.getConversation(id); } else { // load the conversation // PersistentConversation conversation = conversationDao.readConversation(id); // if (conversation == null) { // throw new NoSuchConversationException(id); // } WebFlowConversation webFlowConversation = PersistentConversationHelper.readConversation(id); if (webFlowConversation == null) { throw new NoSuchConversationException(id); } PersistentConversation conversation = new PersistentConversation(webFlowConversation); // cache it for the calling thread PersistentConversationHolder.putConversation(conversation); return conversation; } } public ConversationId parseConversationId(String encodedId) throws ConversationException { // return new SimpleConversationId(conversationIdGenerator.parseUid(encodedId)); return new SimpleConversationId(encodedId); } }- PersistentConversationHolder
Code:public class PersistentConversationHolder { public static final ThreadLocal<Map<ConversationId, PersistentConversation>> conversations = new ThreadLocal<Map<ConversationId, PersistentConversation>>() { protected Map<ConversationId, PersistentConversation> initialValue() { return new HashMap<ConversationId, PersistentConversation>(); } }; public static boolean holdsConversation(ConversationId id) { return conversations.get().containsKey(id); } public static void assertHoldsConversation(ConversationId id) throws NoSuchConversationException { if (!holdsConversation(id)) { throw new NoSuchConversationException(id); } } public static PersistentConversation getConversation(ConversationId id) throws NoSuchConversationException { assertHoldsConversation(id); return conversations.get().get(id); } public static void putConversation(PersistentConversation conversation) throws NoSuchConversationException { conversations.get().put(conversation.getId(), conversation); } public static void removeConversation(ConversationId id) throws NoSuchConversationException { assertHoldsConversation(id); conversations.get().remove(id); } }
Almost I forget... This the script of the table where I am storing the conversation data:
So that's all at the moment. I hope it's useful for others and I expect to receive suggestions and comments for improve the code.Code:CREATE TABLE `web_flow_conversations` ( `ID` varchar(50) NOT NULL, `ATTRIBUTES` blob NOT NULL, PRIMARY KEY (`ID`) )
Thanks.



Reply With Quote
