muh6mm3d / struts2-conversation

Automatically exported from code.google.com/p/struts2-conversation
0 stars 0 forks source link

Automatic creation of conversation #7

Open GoogleCodeExporter opened 9 years ago

GoogleCodeExporter commented 9 years ago
Hi,

I have another suggestion to improve your neat plugin.

In order to start a conversation I have to write a "begin"-Method. Therefore 
all my links on the html page have to point to this method. After calling this 
link I make a redirect to the intended method with the logic. The redirect is 
neccessary because I want the possibility to refresh the site in the browser 
(instead of creating a new conversation).

Here's an example how I solved it now:

@ConversationController
public class ExampleAction extends MyBaseAction {

    private String number;

    @ConversationField
    private ExampleModel exampleModel;

    @BeginConversation
    public String startConversation() {
        exampleModel = new ExampleModel();
        return "refresh";
    }

    @ConversationAction
    public String doSomething() {
        //do something with the data
        return SUCCESS;
    }

    // ...
}

in my struts.xml:
<result type="conversationRedirectAction" name="refresh">
  <param name="namespace">...</param>
  <param name="actionName">...</param>
  <param name="method">doSomething</param>
  <param name="number">${number}</param>
</result>

That means, that a link

/example.action!startConversation?number=123

redirects to

/example.action!doSomething?number=123&example-action_conversation=0

As you can see, there is a lot of boilerplate configuration in the struts.xml.

It would be nice if I can do something like that:

@ConversationController
public class ExampleAction extends MyBaseAction {

    private String number;

    @ConversationField
    private ExampleModel exampleModel;

    @BeginConversationIfNotExist // just a name :)
    public String doSomething() {
        //do something with the data
        return SUCCESS;
    }

    @InitConversation // optional
    private initConversation()
    {
      // similar to the Preparable-Interface
      exampleModel = new ExampleModel();
    }

    // ...
}

When I try to access

/example.action!doSomething?number=123

then the ConversationInterceptor should see that there's no conversation id 
yet. Because of the annotation @BeginConversationIfNotExist it knows that a 
conversation is needed. The interceptor or processor calls the 
initConversation() method to initialize my conversation fields if needed. 
Afterwards it should send a redirect to the browser with all the parameters 
inclusive the new conversation id (it is important that the doSomething method 
isn't called):

/example.action!doSomething?number=123&example-action_conversation=0

This time the interceptor sees that there's a valid conversation id and the 
following process is as usual.

Benefits:
- no redirect configuration for the "begin"-Method is needed (if I want to 
allow a refresh on thesite)
- the link on the page remains the same (bookmarkable, no refactoring if 
conversation is not needed anymore)

My questions:
Do you think it's a good enhancement for conversation plugin?
If yes, do you think it can be implemented/introduced in the near future? 
Otherwise I would implement it myself because I want to ease the use of this 
plugin.

Original issue reported on code.google.com by pepsifa...@googlemail.com on 21 Mar 2013 at 10:00

GoogleCodeExporter commented 9 years ago
I will look into this more soon and update with an estimated level of effort 
and perhaps some feedback on some API details.  This enhancement will not be 
included in the 1.7.4 release, but will instead likely be part of 1.8.0 since 
it may result in an API change.

Feel free to submit a patch if you want ;-)

I will probably put together a first stab in the next couple of weeks and ask 
for some feedback on what I put together.  I would get to it sooner (and I 
might still), but between basketball, work, chores, and a girlfriend I'm a 
little busy over the next week.  

Thanks for the suggestions!

Original comment by reesby...@gmail.com on 23 Mar 2013 at 2:30

GoogleCodeExporter commented 9 years ago
Glad to hear that you consider this suggestion for the next release.

Here is a quick-and-dirty solution, for the ones who don't want to wait :)

I know it's not perfect but it works and should demonstrate the idea.

Some notices:
- It requires the conversation plugin in the version 1.7.3 (which supports 
custom beans, see issue #6)
- No API changes are needed: only ConversationInterecptor and 
ConversationProcessor are overridden
- It reuses the annotation @ConversationAction, a new conversation will be 
started if no exist
- Introduction of the interface ConversaitonPreparable which should initialize 
the ConversationFields
- the maxIdleTime is hard coded

public class CustomConversationInterceptor extends ConversationInterceptor
{
  @Override
  public String intercept(ActionInvocation invocation) throws Exception
  {
    try
    {
      ActionContext ctx = invocation.getInvocationContext();
      HttpServletRequest request = (HttpServletRequest) ctx.get(ServletActionContext.HTTP_REQUEST);

      ConversationContextManager contextManager = contextManagerProvider.getManager(request);
      final ConversationAdapter adapter = new StrutsConversationAdapter(invocation, contextManager);

      try
      {
        processor.processConversations(adapter);
        invocation.addPreResultListener(new PreResultListener()
        {
          @Override
          public void beforeResult(ActionInvocation invocation, String resultCode)
          {
            adapter.executePostProcessors();
            invocation.getStack().getContext()
                .put(StrutsScopeConstants.CONVERSATION_ID_MAP_STACK_KEY, adapter.getViewContext());
          }
        });

        return invocation.invoke();
      }
      catch (ConversationInitializedException e)
      {
        // catch the exception, that means a new conversation was created

        // call the post processor to persist the conversation fields
        adapter.executePostProcessors();
        invocation.getStack().getContext()
            .put(StrutsScopeConstants.CONVERSATION_ID_MAP_STACK_KEY, adapter.getViewContext());

        HttpServletResponse response = (HttpServletResponse) ctx.get(ServletActionContext.HTTP_RESPONSE);
        String location = createRedirectLocation(request);

        // append the conversation id to the redirect url
        String locationWithConversationIds = RedirectUtil.getUrlParamString(location, ConversationAdapter
            .getAdapter().getViewContext());
        String finalLocation = response.encodeRedirectURL(locationWithConversationIds);
        response.sendRedirect(finalLocation);

        // matter of taste...
        return null;
      }
    }
    catch (ConversationIdException cie)
    {
      return this.handleIdException(invocation, cie);
    }
    catch (ConversationException ce)
    {
      return this.handleUnexpectedException(invocation, ce);
    }
    finally
    {
      ConversationAdapter.cleanup();
    }
  }

  /**
   * Recreates the URL from the request
   */
  private static String createRedirectLocation(HttpServletRequest request)
  {
    StringBuffer location = HttpUtils.getRequestURL(request); // HttpUtils is deprecated
    Map<String, String> paramMap = request.getParameterMap();
    if (paramMap.isEmpty())
    {
      return location.toString();
    }
    StringBuilder sb = new StringBuilder();
    sb.append(location.toString());
    sb.append("?");
    for (Iterator<Entry<String, String>> iterator = paramMap.entrySet().iterator(); iterator.hasNext();)
    {
      Entry<String, String> entry = iterator.next();
      sb.append(entry.getKey()).append("=").append(entry.getValue());
      if (iterator.hasNext())
      {
        sb.append("&");
      }
    }
    return sb.toString();
  }
}

public class CustomConversationProcessor extends 
DefaultInjectionConversationProcessor
{
  private static final long serialVersionUID = 1L;

  public void processConversations(ConversationAdapter conversationAdapter) throws ConversationException
  {
    Object action = conversationAdapter.getAction();
    Collection<ConversationClassConfiguration> actionConversationConfigs = this.configurationProvider
        .getConfigurations(action.getClass());

    boolean initialized = false;
    if (actionConversationConfigs != null)
    {
      for (ConversationClassConfiguration conversationConfig : actionConversationConfigs)
      {
        String actionId = conversationAdapter.getActionId();
        String conversationName = conversationConfig.getConversationName();
        String conversationId = (String) conversationAdapter.getRequestContext().get(conversationName);
        if (conversationId == null && conversationConfig.containsAction(actionId))
        {
          long maxIdleTime = 28800000; // TODO the intermediate member doesn't have an idleTime
          ConversationUtil.begin(conversationName, conversationAdapter, maxIdleTime);

          // Call prepare method
          if (action instanceof ConversationPreparable)
          {
            ((ConversationPreparable) action).prepareConversation();
          }

          ConversationContext newConversationContext = ConversationUtil.begin(conversationName, conversationAdapter,
              maxIdleTime);
          conversationId = newConversationContext.getId();
          conversationAdapter.addPostProcessor(this, conversationConfig, conversationId);
          initialized = true;
        }
        else
        {
          processConversation(conversationConfig, conversationAdapter, action);
        }
      }
    }

    if (initialized)
    {
      // I didn't want to change the API, so I throw a runtime exception which is caught by the interceptor
      throw new ConversationInitializedException();
    }
  }
}

public class ConversationInitializedException extends RuntimeException
{
  private static final long serialVersionUID = 1L;
}

public interface ConversationPreparable
{
  void prepareConversation();
}

Example usage:
@ConversationController
public class ExampleAction extends ActionSupport implements 
ModelDriven<ExampleModel>, ConversationPreparable
{
  private String name;

  @ConversationField
  private ExampleModel model;

  @ConversationAction
  public String execute()
  {
    return SUCCESS;
  }

  @ConversationAction
  public String add()
  {
    model.getNames().add(name);
    return "refresh";
  }

  @Override
  public void prepareConversation()
  {
    System.out.println("begin");
    model = new ExampleModel();
  }

  public void setName(String name)
  {
    this.name = name;
  }

  @Override
  public ExampleModel getModel()
  {
    return model;
  }
}

public class ExampleModel
{
  private List<String> names = new LinkedList<String>();

  public List<String> getNames()
  {
    return names;
  }
}

example.jsp:
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib uri="/struts-conversation-tags" prefix="sc"%>   

<h1>Example</h1>
<s:iterator value="names">
  <s:property value="%{toString()}"/><br/>
</s:iterator>
<sc:form namespace="/example" action="example">
<s:textfield name="name"></s:textfield>
<s:submit method="add" value="Add"></s:submit>
</sc:form>

struts.xml:
<struts>
  <!-- need conversation plugin 1.7.3 -->
  <constant name="com.google.code.rees.scope.conversation.processing.ConversationProcessor" value="myCustomProcessor" />
  <bean name="myCustomProcessor" type="com.google.code.rees.scope.conversation.processing.ConversationProcessor"
    class="path.to.CustomConversationProcessor" />

  <package name="default" extends="struts-default">
    <result-types>
      <result-type name="conversationRedirectAction" class="com.google.code.rees.scope.struts2.ConversationActionRedirectResult" />
    </result-types>
    <interceptors>
      <interceptor name="customConversationInteceptor" class="path.to.CustomConversationInterceptor" />
      <interceptor-stack name="customStack">
        <interceptor-ref name="customConversationInteceptor" />
        <interceptor-ref name="defaultStack" />
      </interceptor-stack>
    </interceptors>
    <default-interceptor-ref name="customStack" />
  </package>

  <package name="example" namespace="/example" extends="default">
    <action name="example" class="path.to.ExampleAction">
      <!-- post-direct-get after submit -->
      <result name="refresh" type="conversationRedirectAction">
        <param name="namespace">/example</param>
        <param name="actionName">example</param>
      </result>
      <result name="success">/path/to/example.jsp</result>
    </action>
  </package>
</struts>

Like I've said before, it's only a proof of concept. I think an API change is 
inevitable for a proper implementation.
Take your time, I will support you with feedback :)

Original comment by pepsifa...@googlemail.com on 24 Mar 2013 at 8:42

GoogleCodeExporter commented 9 years ago
Some corrections:
- the createRedirectLocation() method in my example has a bug, 
request.getParameterMap() returns a Map<String, String[]> instead of 
Map<String, String>
- a typo in the notices: issue #5 must be referred

Original comment by pepsifa...@googlemail.com on 24 Mar 2013 at 10:33

GoogleCodeExporter commented 9 years ago
Just to check back, this is still on the radar.  Work should begin this week.

Original comment by reesby...@gmail.com on 7 Apr 2013 at 5:25

GoogleCodeExporter commented 9 years ago
Beginning work on this issue.  

Original comment by reesby...@gmail.com on 20 Apr 2013 at 2:22

GoogleCodeExporter commented 9 years ago
I haven't yet settled on a permanent solution to this that sits right with me 
from a design perspective.  I'm going to go ahead and promote 1.7.4 to staging 
tonight without this enhancement included.

Original comment by reesby...@gmail.com on 21 Apr 2013 at 5:36