jsr372-experts@javaserverfaces-spec-public.java.net

[jsr372-experts] Re: _at_FlowScoped not entirely consistent?

From: arjan tijms <arjan.tijms_at_gmail.com>
Date: Mon, 17 Aug 2015 11:52:45 +0200

Hi,

I looked somewhat deeper into the FlowScoped situation and found the
following JavaDoc in FlowHandler:

"Managed beans annotated with the CDI annotation FlowScoped must be
instantiated upon a user agent's entry into the named scope, and must
be made available for garbage collection when the user agent leaves
the flow."

This seems pretty clear. Entry into named scoped == instantiation,
exit == destroy.

Yet, both Mojarra and MyFaces do not do the instantiation. Both do
destroy the bean at exit (although Mojarra 2.2.11 did not do this, but
that was a regression)

I created a simple test application, consisting of an non-flow view
that's used to jump into the flow, a flow start node, and a second
view in the flow.

Then a @FlowScoped bean is used with an @PostConstruct and @PreDestroy
that prints to std out if it's being called. The start node has a
method to put a value in the bean, and a link to the next view in the
flow, which then prints this value. Finally there's an exit link that
returns us to the non-flow view.

Expected sequence:

1. Jump into flow
2. See @FlowScoped bean created (@PostConstruct called)
3. Execute init command that sets value
4. Navigate to next flow view
5. See value that was set
6. Navigate out of flow
7. See @FlowScoped bean destroyed (@PreDestroy called)

8. Jump into flow again
9. See @FlowScoped bean created again (@PostConstruct called)
10. Navigate to next flow view once more (note, no value set this time)
11. Don't see previous set value



Actual sequence Mojarra 2.2.12/2.3-m03 & MyFaces 2.2.8:

1. Jump into flow
2. @FlowScoped bean NOT created (@PostConstruct NOT called)
3. Execute init command that sets value
4. @FlowScoped bean created via EL reference
5. Navigate to next flow view
6. See value that was set
7. Navigate out of flow
8. See @FlowScoped bean destroyed (@PreDestroy called)

9. Jump into flow again
10. @FlowScoped bean again NOT created (@PostConstruct NOT called)
12. Navigate to next flow view once more (note, no value set)
13. @FlowScoped bean created via EL reference
13. Don't see previous set value

So it seems clear none of the implementations instantiate a bean
eagerly, but they do destroy the bean when the scope ends.


As for the flow scope map, the JavaDoc on FlowHandler says the following:

"The flowScope EL implicit object is also available to store values in
the "current" slope. Values stored in this scope must be made
available for garbage collection when the user agent leaves the flow."

To test this I modified the application so that instead of a
FlowScoped bean the flow scope map is used (via
Application#getFlowHandler()#getCurrentFlowScope()).

When following a similar sequence of steps, the following is expected:

1. Jump into flow
2. Execute init command that sets value (in flow scope map)
3. Navigate to next flow view
4. See value that was set
5. Navigate out of flow

6. Jump into flow again
7. Navigate to next flow view once more (note, no value set this time)
8. Don't see previous set value

In MyFaces this is indeed what happens, but in Mojarra step 8 is:

8. See value that was set in previous flow execution

I assume this is a bug in Mojarra though and not a spec interpretation issue.



For completeness the full code:

Using @FlowScoped:


import static java.lang.System.out;

import java.io.Serializable;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.faces.context.FacesContext;
import javax.faces.flow.FlowScoped;
import javax.inject.Named;

@Named
@FlowScoped("flow")
public class InjectFlowMapBean implements Serializable {

    private static final long serialVersionUID = 1L;

    private String foo;

    @PostConstruct
    public void init() {
        out.println("@PostConstruct called on view " +
FacesContext.getCurrentInstance().getViewRoot().getViewId());
    }

    public void initFoo() {
        foo = "bar";
    }

    public String getFoo() {
        return foo;
    }

    @PreDestroy
    public void destroy() {
        out.println("@PreDestroy called on view " +
FacesContext.getCurrentInstance().getViewRoot().getViewId());
    }

}


start.xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <h:head>
        <title>Flow test</title>
    </h:head>

    <h:body>
        <h:form>
            <h:commandLink value="Enter flow" action="flow" />
        </h:form>
    </h:body>
</html>


flow/flow-flow.xml

<?xml version='1.0' encoding='UTF-8'?>

<faces-config version="2.2"
              xmlns="http://xmlns.jcp.org/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

<flow-definition id="flow">

<flow-return id="flowReturn">
<from-outcome>/start</from-outcome>
</flow-return>

  </flow-definition>

</faces-config>


flow/flow.xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <h:head>
        <title>Flow start</title>
    </h:head>

    <h:body>
        <h:form>
            <h:commandLink value="init Foo"
action="#{injectFlowMapBean.initFoo()}" />
            <br/>
            <h:commandLink value="Next" action="next" />
        </h:form>
    </h:body>
</html>

flow/next.xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <h:head>
        <title>Flow next</title>
    </h:head>

    <h:body>
        foo:#{injectFlowMapBean.foo}
        <h:form>
            <h:commandLink value="Exit flow" action="flowReturn" />
        </h:form>
    </h:body>
</html>


Using flow scope map:

import javax.enterprise.context.RequestScoped;
import javax.faces.context.FacesContext;
import javax.inject.Named;

@Named
@RequestScoped
public class InjectFlowMapBean {

    public void initFoo() {
        FacesContext.getCurrentInstance().getApplication().getFlowHandler().getCurrentFlowScope().put("foo",
"bar");
    }

    public String getFoo() {
        return (String)
FacesContext.getCurrentInstance().getApplication().getFlowHandler().getCurrentFlowScope().get("foo");
    }

}

(views same as given above)

Kind regards,
Arjan Tijms






On Fri, Aug 14, 2015 at 6:57 PM, Josh Juneau <juneau001_at_gmail.com> wrote:
> I think that Arjan's explanation sounds solid. Having one less attribute to
> worry about would certainly make things easier for those getting started.
>
> Best
>
> Josh Juneau
> juneau001_at_gmail.com
> http://jj-blogger.blogspot.com
> https://www.apress.com/index.php/author/author/view/id/1866
>
>
> On Thu, Aug 13, 2015 at 2:27 AM, arjan tijms <arjan.tijms_at_gmail.com> wrote:
>>
>> Hi,
>>
>> On Thu, Aug 13, 2015 at 12:34 AM, Edward Burns <edward.burns_at_oracle.com>
>> wrote:
>> > Spec> FlowScoped is a CDI scope that causes the runtime to consider
>> > Spec> classes with this annotation to be in the scope of the specified
>> > Spec> Flow. [...] beans with this annotation are created when
>> > Spec> the user enters into the specified Flow, and de-allocated when the
>> > Spec> user exits the specified Flow.
>> >
>> > But I don't think it's 100% eager. I think they only get created when
>> > something such as EL tries to access it.
>>
>> If they only get created upon access, then isn't it the same as with
>> every scope that doesn't require an ID?
>>
>> E.g.
>>
>> @Named
>> @RequestScoped
>> public class Foo {}
>>
>> ...
>>
>> somePage.xhtml:
>>
>> #{foo.toString()} <---- gets created at this point
>>
>> It's not necessary here to use @RequestScoped("somePage").
>>
>> And now it's seemingly the same with @FlowScoped:
>>
>> @Named
>> @FlowScoped("bar")
>> public class Foo {}
>>
>> ...
>>
>> somePage.xhtml:
>>
>> #{foo.toString()} <---- gets created at this point
>>
>>
>> At this point the id "bar" is not used at all, since it's the name of
>> the bean "foo" and the EL reference to it that caused the bean to be
>> created on demand.
>>
>>
>> With an id you could theoretically do the following:
>>
>> Suppose a flow with view bar.xhtml and next.xhtml both in the flow.
>>
>> @Named
>> @FlowScoped("bar")
>> public class Foo {}
>>
>> ...
>>
>> bar.xhtml
>>
>> (bean "foo" is created when the user enters flow "bar", since the
>> system can look up the bean by its flow id)
>>
>> somePage.xhtml:
>>
>> #{foo.toString()} <---- already exists at this point
>>
>>
>>
>> > Finally, the main reason the value is necessary is that how else can a
>> > FlowScoped bean be declared to be in a scope?
>>
>> I think just by using @FlowScoped:
>>
>> @Named
>> @FlowScoped
>> public class Foo {}
>>
>> With that the bean "foo" is scoped to whatever the current active flow
>> is. Technically this is already what happens.
>>
>> The current implementation code doesn't actually use the flow id on
>> @FlowScoped, except for a validation that the current flow id matches
>> with the id (value) of the @FlowScoped annotation. But this is only a
>> check. If you remove the check and recompile the code everything still
>> seems to work. Which leads to the question, why is the check there in
>> the first place?
>>
>> #{flowScope} is a Map<Object, Object> that's effectively flow scoped
>> too, but this one does not define a flow id. It simply resolves to the
>> "current" flow.
>>
>> Now as a consequence there is a small problem when you want to make a
>> dynamic producer (Bean<T>) for #{flowScope}. You need to set the scope
>> of this to @FlowScoped (no value possible here, and even if possible
>> not wanted):
>>
>> public class FlowMapProducer extends CdiProducer<Map<Object, Object>> {
>>
>> public FlowMapProducer() {
>> super.name("flowScope")
>> .scope(FlowScoped.class) // <------- problematic with
>> current implementation
>> .qualifiers(new FlowMapAnnotationLiteral())
>> .types(
>> new ParameterizedTypeImpl(Map.class, new
>> Type[]{Object.class, Object.class}),
>> Map.class,
>> Object.class)
>> .beanClass(Map.class)
>> .create(e ->
>>
>> FacesContext.getCurrentInstance().getApplication().getFlowHandler().getCurrentFlowScope());
>> }
>>
>> }
>>
>> In this case the Map needs to be retrieved from the *current* flow
>> scope, and so for this definition it makes no sense to put a value
>> that represents a specific flow id somewhere.
>>
>> This case too works when I remove the check for the matching flow IDs.
>> (there's an alternative solution as well, but that too boils down to
>> simply not doing the check).
>>
>> Kind regards,
>> Arjan Tijms
>>
>>
>>
>>
>>
>>
>> >
>> > Ed
>> >
>> > --
>> > | edward.burns_at_oracle.com | office: +1 407 458 0017
>> > | 59 Business days til JavaOne 2015
>> > | 74 Business days til DOAG 2015
>
>