Page 1 of 2 12 LastLast
Results 1 to 10 of 17

Thread: Spring Webflow 2 Working With Persistent Conversation Data

  1. #1

    Default Spring Webflow 2 Working With Persistent Conversation Data

    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:
    Code:
    CREATE TABLE `web_flow_conversations` (
      `ID` varchar(50) NOT NULL,
      `ATTRIBUTES` blob NOT NULL,
      PRIMARY KEY (`ID`)
    )
    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.

    Thanks.
    Last edited by alejandrogarciaseco; Sep 25th, 2012 at 08:03 AM. Reason: Forgot something...

  2. #2

    Default

    Hi Alejandro
    I was so relieved to find this post as i have been googling for this functionality for past few days and not much help.

    I am also working on SWF2 and have same requirement but i am not that much acquainted with SWF's flow persistence and still learning.

    Please post the more code (if possible) if you were able to make it working.Or if you can provide some more details .

    Thanks

  3. #3

    Default

    Quote Originally Posted by Misha79 View Post
    Hi Alejandro
    I was so relieved to find this post as i have been googling for this functionality for past few days and not much help.

    I am also working on SWF2 and have same requirement but i am not that much acquainted with SWF's flow persistence and still learning.

    Please post the more code (if possible) if you were able to make it working.Or if you can provide some more details .

    Thanks
    Hi Misha79,

    I'm proud about you found my post useful.

    I was able to make it work and it's currently working in my application correctly in a development environment. At the moment it seems stable and performs pretty good, despite of I am not still happy at all with the implementation, in my opinion it should be revised in terms of efficiency and elegance.

    Well, what do you need exactly? What's missing in your opinion? Make me know and I will complete the post.

    By the way, I strongly recommend you downloading and studying the source code of SWF1, SWF2 as well as the original Erwin's example of persistent conversation data on SWF1. The Erwin's book is also an interesting tool.

    Regards.

  4. #4

    Default

    Thanks Alejandro,
    For replying as i was desparately checking reply to this post everyday.Finally i starting working on something else and today i saw the reply and i was very happy.


    Well my requirement is:--

    I have flow in which user can save and exit anytime from any page in flow.
    Once he logins inback to the application,i want user to be directed to same pagenumber where he 'saved & exited' from.
    Also all the user-entries to pages should be saved in db and will be retained when he logs in.
    Eg He exits from PPage num 5 and logs out.He logs in and he directly sees page num 5 and he can still navigate back to pages 4,3,1 by clicking 'back' buttons and he should see data he entered before.

    I have gone through 'Erwins Definitive guide' and which is very informative.I started developing my code as per your post & code snippets but couldnot make it work .

    First of all i am not even able to configure the webflow.xml .

    I have custom java files for
    -FlowExecutionRepository
    -Conversation
    -conversationDao
    -conversationHelper
    -conversationManager
    -JdkConcurrentConversationLock (as your example)

    My xml - bold line has some issues.

    Code:
       <!-- Executes flows: the entry point into the Spring Web Flow system -->
       
         <webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry"    >
            <webflow:flow-execution-listeners>
                <webflow:listener ref="customFloeExecutionListener" criteria="main-flow"/>
            </webflow:flow-execution-listeners>
          <webflow:flow-execution-repository   conversation-manager="customConversationManager" max-executions="5" >
         </webflow:flow-execution-repository>
            
        </webflow:flow-executor>
    
    
    
    <bean id="customFlowExecutionRepository" class="com.persistent.flows..CustomFlowExecutionRepository">
    	<constructor-arg>
    	<bean class="org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer">
    			<constructor-arg ref="flowRegistry"/>
    			<property name="executionAttributesMap" ref="executionAttributes"/>
    		</bean>
    	</constructor-arg>
    	<constructor-arg ref="customConversationManager"/>
    	
    </bean>

    I am going to read everything again over the weekend and restart.

    Thanks again .





    Quote Originally Posted by alejandrogarciaseco View Post
    Hi Misha79,

    I'm proud about you found my post useful.

    I was able to make it work and it's currently working in my application correctly in a development environment. At the moment it seems stable and performs pretty good, despite of I am not still happy at all with the implementation, in my opinion it should be revised in terms of efficiency and elegance.

    Well, what do you need exactly? What's missing in your opinion? Make me know and I will complete the post.

    By the way, I strongly recommend you downloading and studying the source code of SWF1, SWF2 as well as the original Erwin's example of persistent conversation data on SWF1. The Erwin's book is also an interesting tool.

    Regards.

  5. #5

    Default

    About webflow configuration, I have few questions.
    I have customFlowExecutionRepository which extends DefaultFlowExecutionRepository and it is annotated too. Also in xml i configured it as bean ( which was not required).
    Similary, I have customConversationManager configured in xml but dont know how to configure customFlowExecution Repository.
    In code - type= "customFlowExecutionRepository" wont work .
    Code:
    <webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry"    >
         <webflow:flow-execution-repository conversation-manager="customConversationManager" 
         max-executions="5" max-execution-snapshots="0" 
         type= "customFlowExecutionRepository"
         
         />
            <webflow:flow-execution-listeners>
                <webflow:listener ref="customFlowExecutionListener" criteria="main-flow"/>
            </webflow:flow-execution-listeners>
            
            <webflow:flow-execution-attributes>
            	<webflow:always-redirect-on-pause value="false" />
        	</webflow:flow-execution-attributes> 
        	    
        </webflow:flow-executor>

  6. #6

    Default

    Well Misha79, it seems you have exactly the same requirements than me so, if I was able to make it work properly you can too!

    Regarding to your issue about configuration, I can tell you that I'm configuring it in a slightly different way. This is the XML snippet for the flow executor with the custom conversation manager:

    Code:
    	<!-- Creates a flow executor in Spring, responsible for creating and executing flows -->
    	<webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry">
    		<!-- Specifies a custom conversation manager and limits the snapshots creation for flows instances -->
    		<webflow:flow-execution-repository max-execution-snapshots="5" conversation-manager="persistentConversationManager" />
    	</webflow:flow-executor>
    And my custom conversation manager is annotated like this:

    Code:
    @Component(value = "persistentConversationManager")
    public class PersistentConversationManager implements ConversationManager {...}
    If you are using annotations you don't need to explicitly declare in the XML your custom conversation manager for wiring.

    By the way, which version of SWF are you working with? And what kind of problem are you facing? Are you having an exception or just it isn't working and nothing else? Try to give me a more detailed explanation of your problem please.

    Good weekend.

  7. #7

    Default

    Hi Alejandro
    I tried exactly same as your xml but when i tried debugging, i dint see control coming to my custom conversation manager class which means it dint bother about my conversation manager. From furthur research , i realized i need to add custom flow execution repository
    which cant be added just like that and i followed post here created like that-
    Code:
    public class  CustomFlowExecutorFactoryBean implements FactoryBean{
    	
    	 private FlowExecutorImpl defaultFlowExecutor;
    	 private Object singleton;
    
    	@Override
    	public Object getObject() throws Exception {
    		 if (defaultFlowExecutor == null) {
    	            throw new FactoryBeanNotInitializedException("defaultFlowExecutor is null ");
    	        }
    	        
    	        if (singleton != null) { return singleton; }
    	        FlowExecutorImpl lFEx = defaultFlowExecutor;
    	        FlowDefinitionLocator lOriginalLocator = lFEx.getDefinitionLocator();
    	        FlowExecutionFactory lOriginalFactory = lFEx.getExecutionFactory();
    	        FlowExecutionRepository lCtqFER = 
    	            new CustomFlowExecutionRepository((DefaultFlowExecutionRepository)lFEx.getExecutionRepository());
    	        singleton = new FlowExecutorImpl(lOriginalLocator, lOriginalFactory, lCtqFER);
    	        return singleton;
    	}
    
    	@Override
    	public Class getObjectType() {
    		return FlowExecutorImpl.class;
    	}
    
    	@Override
    	public boolean isSingleton() {
    		
    		return true;
    	}
    	 /**
         * @param pDefaultFlowExecutor the defaultFlowExecutor to set
         */
    	
        public void setDefaultFlowExecutor(FlowExecutorImpl pDefaultFlowExecutor) {
            defaultFlowExecutor = pDefaultFlowExecutor;
        }
    
    }
    
    
    and tweaked xml -
    <bean id="flowController" class="org.springframework.webflow.mvc.servlet.FlowController">
    	    <property name="flowExecutor"  >
    	    <bean id="customFlowExecutor" class="com.myflows.customFlowExecutorFactoryBean">
        		 <property name="defaultFlowExecutor" ref="flowExecutor1">
        		</property>
        	</bean>
    	    </property>
    	    <property name="flowUrlHandler">
    			<bean id="webFlowUrlHandler" class="org.springframework.webflow.context.servlet.WebFlow1FlowUrlHandler"/>
    	    </property>
    	</bean>
    	
    	
    	   <!-- Executes flows: the entry point into the Spring Web Flow system -->
       
         <webflow:flow-executor id="flowExecutor1" flow-registry="flowRegistry"  >
         	<webflow:flow-execution-repository conversation-manager="customPersistentConversationManager"      
         			max-executions="5" max-execution-snapshots="0"              />
            	<webflow:flow-execution-listeners>
                	<webflow:listener ref="customFlowExecutionListener" criteria="root-flow"/>
            	</webflow:flow-execution-listeners>
            
            	<webflow:flow-execution-attributes>
            		<webflow:always-redirect-on-pause value="false" />
        		</webflow:flow-execution-attributes> 
        </webflow:flow-executor>
    And ran into NullPointerException in beginning only.
    also about version, I am actually using SWF2.0 but for this functionality, i added SWF2.3 jars and modified schema reference in xml to SWF 2.3
    With SWF 2.0- "conversation-manager" attribute gives errors in xml but it works with SWF 2.3
    Code:
    <webflow:flow-execution-repository conversation-manager="customPersistentConversationManager"      
         			max-executions="5" max-execution-snapshots="0"              />
    Otherwise my xml tags were showing errors and i was'nt not at all able to figure out.
    Last edited by Misha79; Sep 9th, 2012 at 04:27 PM.

  8. #8

    Default

    Hi,
    I just figured that my custom conversation manager is not at all being detected.(this is without custom flow execution
    repository code or setup) .
    I can type anything to the value(real id or xyz) of attribute conversation-manager ,but i donot get any error.
    This is SWF 2.3


    Code:
     <webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry"  >
         	<webflow:flow-execution-repository conversation-manager="type-anything-here-not-detected"      
         			max-executions="5"              />
            	<webflow:flow-execution-listeners>
                	<webflow:listener ref="flowExecutionListener" criteria="main-flow"/>
            	</webflow:flow-execution-listeners>
            
            	<webflow:flow-execution-attributes>
            		<webflow:always-redirect-on-pause value="false" />
        		</webflow:flow-execution-attributes> 
        </webflow:flow-executor>
    Please suggest.

  9. #9

    Default

    Hi Misha79,

    Which namespaces are you using in in your XML? With SWF 2.3 I'm using the next ones:

    Code:
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:mvc="http://www.springframework.org/schema/mvc"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:webflow="http://www.springframework.org/schema/webflow-config"
    	xsi:schemaLocation="
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
            http://www.springframework.org/schema/webflow-config http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd">
    Anyway, I have to make you know that sometimes I also have XML errors related to the webflow: namespace, but I really don't know why. Nevertheless, when I deploy the project to my Tomcat 6.0 it works perfectly.

    By the way, if you want to use annotations you should add those tags at the beginning of your XML file:

    Code:
    	<!-- Scans within the base package of the application for @Components to configure as beans -->
    	<!-- @Controller, @Service, @Configuration, etc. -->
    	<context:component-scan base-package="ie.i2e2.greenmode.web" />
    	
    	<!-- Enables the Spring MVC @Controller programming model -->
    	<mvc:annotation-driven conversion-service="applicationConversionService" />
    Otherwise, you would declare your custom conversation manager explicitly as you originally posted.

    At the same time, I see that you are configuring your flow-execution-repository in a different way than me; you are specifying flow execution listener and attributes. I don't know how it could affect to the performance, but take it into account. Initially, if I were you I would try to configure SWF in a way as closer as possible and then once it is working, I would add my custom preference step by step checking always that it keeps working. If you want, I can post my whole dispatcher XML.

    By the way, are you using Maven? I don't believe your problem is libraries issue, but I could post my dependencies to make you able to set up an environment as similar to mine.

    Regards.

  10. #10

    Default conversation manager is being identified by spring now.

    My app is picking up custom conversation manager now. I had both versions for SWF in my libraries.
    Now i get this exception- means i have to implement .for create/get conversation
    ----------------------
    Code:
    org.springframework.webflow.conversation.NoSuchConversationException: No conversation could be found with id '19005da1-42e0-4bb5-8b1b-cb8b1eb57721' -- perhaps this conversation has ended?
    ------------------------
    Thank you so much. I was struggling for weeks to bring it up.

    For now, I wrote my DAO to read/write onto a file & will be replaced with real DB call soon.
    Once it is better position, i will make DB changes. (Its not easy step to modify DB in my case)

    PersistentConversationHelper - As per your code, should have read/write/update/delete conversation functions.
    I am trying to fillup code for saving/read conversations now.
    Can you please help move little furthur with this?


    Code:
    @Component(value = "csPersistentConversationHelper")
    public class CustomPersistentConversationHelper  {
    	
    	public static void createConversation(
    			CustomPersistentConversation csPersistentConversation) {
    		System.out.println("creating conversation");
    		//??what next...I  have conversationId in conversationmanager
    		
    		
    	}
    
    	public static WebFlowConversation readConversation(ConversationId id) {
    		// TODO Auto-generated method stub
    		return null;
    	}
    
    	public static void deleteConversation(ConversationId id) {
    		// TODO Auto-generated method stub
    		
    	}
    
    	public static void updateConversation(
    			CustomPersistentConversation csPersistentConversation) {
    		// TODO Auto-generated method stub
    		
    	}
    
    	
    }
    On your question, We are not using maven.

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •