Conversations (Draft)

Basic Idea

Std. CDI conversations are very limited. So CODI will provide conversations which are similar to the conversations of Orchestra.
Basically each window has its own context (see WindowContext - it's similar to the ConversationContext of Orchestra).
Within this context it's possible to define multiple conversations (grouped by a typesafe conversation-group instead of a string based name).

The following part explains the available annotations. (All scopes are stored in the session - so the max. lifetime is restricted by the session of the user.)

@ConversationScoped

Beans which are marked with this annotations will be scoped as a grouped conversation. That means the bean exists as one of n (parallel) conversations in the WindowContext. The group is identified via the class of the bean and the qualifiers of the bean.

Example:

@ConversationScoped
public class ConversationDemoBean1 implements Serializable
{
}

If the bean isn't used any more it will be un-scoped automatically after a configurable timeout (default: 30 min).

@ConversationGroup

Furthermore it's possible to create conversation groups (it's a std. CDI qualifier annotation which is handled in a special way). That means 1-n beans will exist in the same conversation. As soon as the conversation gets destroyed all beans will be un-scoped.

Example:

@ConversationScoped
@ConversationGroup(Group1.class)
public class ConversationDemoBean1 implements Serializable
{
}

@ConversationScoped
@ConversationScoped(Group1.class)
public class ConversationDemoBean2 implements Serializable
{
}

TODO: test if the current approach works also with dependency injection in other beans.

Both beans of example 2 will be scoped in a conversation with the group-key: Group1.class

Some important use-case for groups are e.g. wizards, master/detail views,... (e.g. it's possible to use one page-bean for each wizard step. All page-beans used for the wizard are scoped in the same conversation group. At the end of the wizard we can destroy the conversation group of the wizard and every bean gets un-scoped. Furthermore, conversation groups allow an easier API for the WindowContext.

@WindowScoped

The window scope is like a session per window. (It's just a special conversation without timeout). All beans exist as long as the WindowContext is active (or the session of the user).

We will see an API for destroying the WindowContext as a whole or just all window scoped beans. However, usually you don't need to use it.

We can use the window scope e.g. for the current user. So it's possible to support e.g. multiple tabs with different logins.

Example:

@WindowScoped
public class WindowDemoBean implements Serializable
{
}

@ViewAccessScoped

This scope is similar to the access scope of Orchestra. That means: a bean will be available in the next request if it is used in the current request.

We have to support AJAX so the name as well as the rule is a bit different: All beans which are used in a view will be available in the current view (e.g. multiple AJAX requests) as well as in the next view. (Redirects are supported as well.)

Example Use-Case:

views:           [ View 1 ]   [ View 1 ]   [ View 2     ]   [ View 3     ]

used beans:      [ Bean 1 ]   [ Bean 1 ]   [ Bean 2     ]   [ Bean 3     ]

available beans: [ Bean 1 ]   [ Bean 1 ]   [ Bean 1 & 2 ]   [ Bean 2 & 3 ]

Example:

@ViewAccessScoped
public class ViewAccessDemoBean implements Serializable
{
}

1 Bean and N Scopes

Via producer methods it's possible to create multiple beans with the same type but different scopes.

Example:

public class MixedConversationDemoBean implements Serializable
{
    private String value;

    protected MixedConversationDemoBean()
    {
    }

    private MixedConversationDemoBean(String value)
    {
        this.value = value;
    }

    @Produces
    @Qualifier1
    @WindowScoped
    public MixedConversationDemoBean createWindowScopedBean()
    {
        return new MixedConversationDemoBean("Q1@WindowScoped ");
    }

    @Produces
    @Qualifier2
    @ViewAccessScoped
    public MixedConversationDemoBean createViewAccessScopedBean()
    {
        return new MixedConversationDemoBean("Q2@ViewAccessScoped ");
    }

    public String getValue()
    {
        return value;
    }
}

APIs

Core-Module

Annotations

Interfaces

Conversation

The central interface for a conversation which is a container for 1-n beans which share the same time for destruction.

public interface Conversation extends Serializable
{
    /**
     * Deactivates the conversation and un-scopes all bean instances immediately.<br/>
     * At the next cleanup the whole conversation will be destroyed.
     * (If an inactive {@link org.apache.myfaces.extensions.cdi.core.api.scope.conversation.Conversation}
     * gets resolved before the cleanup, the
     * {@link org.apache.myfaces.extensions.cdi.core.api.scope.conversation.WindowContext} has to destroy it.
     * -> A new conversation will be created immediately.
     */
    void end();

    /**
     * Un-scopes all bean instances immediately.
     * Instead of destroying the whole conversation the conversation stays active.
     * (The conversation will be marked as used.)<br/>
     * As soon as an instance of a bean is requested,
     * the instance will be created based on the original bean descriptor.
     * This approach allows a better performance, if the conversation is needed immediately.
     */
    void restart();
}

WindowContext

It's like a session per window.

If you need the id of the current WindowContext you can use: #{currentWindowContext.id}

public interface WindowContext extends AttributeAware, Serializable
{
    String CURRENT_WINDOW_CONTEXT_BEAN_NAME = "currentWindowContext";

    /**
     * @return the id of the conversation (unique for each window/tab)
     */
    String getId();

    /**
     * invalidate all conversations immediately
     * attention: window scoped beans won't get destroyed.
     * currently there is no api for it.
     * (if it is needed you have to call {@link #endConversation} in combination with the {@link WindowScoped})
     */
    void endConversations();

    /**
     * @param conversationGroup group of the conversation in question
     * @param qualifiers optional qualifiers for the conversation
     * @return the removed conversation - null otherwise
     */
    Conversation endConversation(Class conversationGroup, Annotation... qualifiers);

    /**
     * destroys all conversation of a group independent of the qualifiers
     * @param conversationGroup group of the conversation in question
     * @return the removed conversation - null otherwise
     */
    Set<Conversation> endConversationGroup(Class conversationGroup);

    /**
     * @return configuration of the current context
     */
    WindowContextConfig getConfig();
}

The following example shows how to use the current WindowContext

public class DemoBean
{
    @Inject
    private WindowContext windowContext;

    public void endConversationGroup1()
    {
        this.windowContext.endConversationGroup(ConversationGroup1.class);
    }
}

WindowContext - Advanced Topics

By default we have to add a window-id to the url automatically. Furthermore, (currently) the first request has to force a redirect to force a window id also for the first page. If you don't like a window-id in your url/s, you have to switch to a server-side solution. MyFaces CODI provides the ServerSideWindowHandler which uses the flash-scope of JSF2. That means the JSF version you are using needs a working flash-scope! (os890 provides a 2nd alternative.) However, if you store the window-id via a server-side mechanism you lose the support for window refreshes.

We need a window-id for all pages (similar to MyFaces Orchestra). Reason: if you would have a lazy window-id which is just active on pages which use conversation scoped beans, you might get strange problems. E.g. if Page A doesn't use conversations and Page B starts using conversations and you navigate back to Page A - also Page A needs the window-id. If Page A doesn't supports the window-id (e.g. a menu which uses GET-Requests), the navigation would lead to a broken application. In this case it's just broken in special constellations. Such issues are very hard to find. So we have to force the window-id for all pages. By default you don't have to adjust your pages - just if you have e.g. a GET-Request based menu). That means we are using a fail fast approach (as fast as possible) and you have to refactor such a page - e.g. add ?windowId=#{currentWindowContext.id} to the entries of a menu which uses GET-Requests. (There might be a ViewDefinition based solution for pages which are just available for external applications and not part of the nav.-flow within the application.)

Currently the initial redirect is activated by default - you can deactivate it via: org.apache.myfaces.extensions.cdi.DISABLE_INITIAL_REDIRECT (that might change - see EXTCDI-44)

If you have an external application which isn't able to handle redirects, you can use: windowId=automatedEntryPoint as url parameter.

If you have a 2nd application which has to propagate it's own window-id to your application, you have to deactivate trusted window-ids via: org.apache.myfaces.extensions.cdi.ALLOW_UNKNOWN_WINDOW_IDS

If your component lib is aware of windows, you can impl. a cusotm WindowHandler which delegates the work to the component lib.

Further readings

Injecting the current conversation

Example

@ConversationScoped
public class DemoBean implements Serializable
{
    @Inject
    private Conversation conversation;

    public void restartConversation()
    {
        //this.conversation.end();
        this.conversation.restart();
    }
}

Configuration

Currently we have got: WindowContextConfig. It isn't finished - so we have to think about further config entries.

Events

The base class is ConversationEvent. Currently we have got the following event types:

For sure the lifecycle annotations @PostConstruct and @PreDestroy are supported as well.

SPIs

Core-Module

It's possible to implement a custom WindowContextManager. Basically a WindowContextManager is responsible for resolving the current WindowContext (or a context for a given ID). It's possible to activate or remove a special WindowContext. Furthermore, it's possible to reset a WindowContext as well as all Conversations.

However, this part might change -> further details are available in the svn-trunk.

JSF-Module

This part might change -> further details are available in the svn-trunk.

https://issues.apache.org/jira/browse/EXTCDI-1 https://issues.apache.org/jira/browse/EXTCDI-2 https://issues.apache.org/jira/browse/EXTCDI-3 https://issues.apache.org/jira/browse/EXTCDI-32

Needed Test-Cases

All tests should work with the default (= url encoding) as well as with a server-side window-handler (requires a working flash scope or the version of os890).

All use-cases which are possible via Ajax requests should be tested with full as well was partial requests.

Initial request triggers redirect to url with window-id.

Request to the same page.

Navigation to a 2nd page and 3rd page -> view-access scoped beans of 1st page should be destroyed (via forwards as well as via redirects)

Navigation to a 2nd page and back to the 1st page should work

An expired window which gets reused should get a new window context with the same (old) window-id (to avoid an additional redirect).

A bookmark of an unknown window-id should trigger the creation of a new window-context

Support for view-parameters

Test for org.apache.myfaces.extensions.cdi.DISABLE_INITIAL_REDIRECT

Test for org.apache.myfaces.extensions.cdi.ALLOW_UNKNOWN_WINDOW_IDS

Extensions/CDI/DevDoc/Conversations (last edited 2010-08-18 17:04:17 by 188-23-75-9)