4

I'm trying to port the JSF @ViewScoped annotation to CDI. The reason is more educational rather than based on need. I chose this particular scope mainly due to the lack of a better concrete example of a custom scope one might want to implement in CDI.

That said, my starting point was Porting the @ViewScoped JSF annotation to CDI. But, this implementation does not take into account a seemingly very important responsibility of Context (i.e. destroying) mentioned in the API:

The context object is responsible for creating and destroying contextual instances by calling operations of Contextual. In particular, the context object is responsible for destroying any contextual instance it creates by passing the instance to Contextual.destroy(Object, CreationalContext). A destroyed instance must not subsequently be returned by get(). The context object must pass the same instance of CreationalContext to Contextual.destroy() that it passed to Contextual.create() when it created the instance.

I decided to add this functionality by having my Context object:

  1. keep track of what Contextual objects it creates for which UIViewRoots;
  2. implement the ViewMapListener interface and register itself as listener for each UIViewRoot by calling UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this);
  3. destroy any created Contextuals when the ViewMapListener.processEvent(SystemEvent event) is called and unregister itself from that UIViewRoot.

Here's my Context implementation:

package com.example;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PreDestroyViewMapEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.ViewMapListener;

public class ViewContext implements Context, ViewMapListener {

    private Map<UIViewRoot, Set<Disposable>> state;

    public ViewContext() {
        this.state = new HashMap<UIViewRoot, Set<Disposable>>();
    }

    // mimics a multimap put()
    private void put(UIViewRoot key, Disposable value) {
        if (this.state.containsKey(key)) {
            this.state.get(key).add(value);
        } else {
            HashSet<Disposable> valueSet = new HashSet<Disposable>(1);
            valueSet.add(value);
            this.state.put(key, valueSet);
        }
    }

    @Override
    public Class<? extends Annotation> getScope() {
        return ViewScoped.class;
    }

    @Override
    public <T> T get(final Contextual<T> contextual,
            final CreationalContext<T> creationalContext) {
        if (contextual instanceof Bean) {
            Bean bean = (Bean) contextual;
            String name = bean.getName();
            FacesContext ctx = FacesContext.getCurrentInstance();
            UIViewRoot viewRoot = ctx.getViewRoot();
            Map<String, Object> viewMap = viewRoot.getViewMap();
            if (viewMap.containsKey(name)) {
                return (T) viewMap.get(name);
            } else {
                final T instance = contextual.create(creationalContext);
                viewMap.put(name, instance);
                // register for events
                viewRoot.subscribeToViewEvent(
                        PreDestroyViewMapEvent.class, this);
                // allows us to properly couple the right contaxtual, instance, and creational context
                this.put(viewRoot, new Disposable() {

                    @Override
                    public void dispose() {
                        contextual.destroy(instance, creationalContext);
                    }

                });
                return instance;
            }
        } else {
            return null;
        }
    }

    @Override
    public <T> T get(Contextual<T> contextual) {
        if (contextual instanceof Bean) {
            Bean bean = (Bean) contextual;
            String name = bean.getName();
            FacesContext ctx = FacesContext.getCurrentInstance();
            UIViewRoot viewRoot = ctx.getViewRoot();
            Map<String, Object> viewMap = viewRoot.getViewMap();
            if (viewMap.containsKey(name)) {
                return (T) viewMap.get(name);
            } else {
                return null;
            }
        } else {
            return null;
        }
    }

    // this scope is only active when a FacesContext with a UIViewRoot exists
    @Override
    public boolean isActive() {
        FacesContext ctx = FacesContext.getCurrentInstance();
        if (ctx == null) {
            return false;
        } else {
            UIViewRoot viewRoot = ctx.getViewRoot();
            return viewRoot != null;
        }
    }

    // dispose all of the beans associated with the UIViewRoot that fired this event
    @Override
    public void processEvent(SystemEvent event)
            throws AbortProcessingException {
        if (event instanceof PreDestroyViewMapEvent) {
            UIViewRoot viewRoot = (UIViewRoot) event.getSource();
            if (this.state.containsKey(viewRoot)) {
                Set<Disposable> valueSet = this.state.remove(viewRoot);
                for (Disposable disposable : valueSet) {
                    disposable.dispose();
                }
                viewRoot.unsubscribeFromViewEvent(
                        PreDestroyViewMapEvent.class, this);
            }
        }
    }

    @Override
    public boolean isListenerForSource(Object source) {
        return source instanceof UIViewRoot;
    }

}

Here's the Disposable interface:

package com.example;

public interface Disposable {

    public void dispose();

}

Here's the scope annotation:

package com.example;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.enterprise.context.NormalScope;

@Inherited
@NormalScope
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD,
    ElementType.FIELD, ElementType.PARAMETER})
public @interface ViewScoped {

}

Here's the CDI extension declaration:

package com.example;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.Extension;

public class CustomContextsExtension implements Extension {

    public void afterBeanDiscovery(@Observes AfterBeanDiscovery event) {
        event.addContext(new ViewContext());
    }

}

I added the javax.enterprise.inject.spi.Extension file under META-INF/services containing com.example.CustomContextsExtension to properly register the above with CDI.

I can now make beans like (notice the use of the custom @ViewScoped implementation.):

package com.example;

import com.concensus.athena.framework.cdi.extension.ViewScoped;
import java.io.Serializable;
import javax.inject.Named;

@Named
@ViewScoped
public class User implements Serializable {
    ...
}

The beans are created properly and properly injected into JSF pages (i.e. the same instance is returned per view, new ones are created only when the view is created, the same instances are injected over multiple requests to the same view). How do I know? Imagine the above code littered with debugging code which I purposefully stripped out for clarity and since this is already a huge post.

The problem is that my ViewContext.isListenerForSource(Object source) and ViewContext.processEvent(SystemEvent event) are never called. I was expecting that at least upon session expiration those events would be called, since the view map is stored in the session map (correct?). I set the session timeout to 1 minute, waited, saw the timeout happen, but my listener was still not called.

I also tried adding the following to my faces-config.xml (mostly out of the lack of ideas):

<system-event-listener>
    <system-event-listener-class>com.example.ViewContext</system-event-listener-class>
    <system-event-class>javax.faces.event.PreDestroyViewMapEvent</system-event-class>
    <source-class>javax.faces.component.UIViewRoot</source-class>
</system-event-listener>

Finally, my environment is JBoss AS 7.1.1 with Mojarra 2.1.7.

Any clues would be greatly appreciated.

EDIT: Further Investigation.

PreDestroyViewMapEvent doesn't seem to be fired at all while PostConstructViewMapEvent is fired as expected - every time a new view map is created, specifically during UIViewRoot.getViewMap(true). The documentation states that PreDestroyViewMapEvent should be fired every time clear() is called on the view map. That leaves to wonder - is clear() required to be called at all? If so, when?

The only place in the documentation that I was able to find such a requirement is in FacesContext.setViewRoot():

If the current UIViewRoot is non-null, and calling equals() on the argument root, passing the current UIViewRoot returns false, the clear method must be called on the Map returned from UIViewRoot#getViewMap.

Does this ever happen in the normal JSF lifecycle, i.e. without programmatically calling UIViewRoot.setViewMap()? I can't seem to find any indication.

rdcrng
  • 3,415
  • 2
  • 18
  • 36
  • 1
    Have you seen the version in MyFaces CODI? That may help point you in the correct direction. – LightGuard Nov 15 '12 at 07:54
  • Now that you mentioned it - I did. I downloaded their sources and inspected them. Their implementation for the view context is very similar to what I have above. The key similarity is that they register for JSF events this way `FacesContext.getCurrentInstance().getApplication().subscribeToEvent(PreDestroyViewMapEvent.class, this);` and implement `SystemEventListener`. They way I do it should be exactly the same, since the API states that invocations of the `UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this);` should call through to the other method. @LightGuard – rdcrng Nov 15 '12 at 15:21
  • Furthermore, they implement `SystemEventListener` while I implement `ViewMapListener` which is just a subinterface, so there is no problem there. So it seems there is nothing terribly wrong with my implementation. The problem is that my JSF environment doesn't fire any system events, whether I register them programmatically or through `faces-config.xml`. Any idea why this may be happening? Thanks for suggesting MyFaces CODI @LightGuard – rdcrng Nov 15 '12 at 15:25
  • That's odd because I know the CODI one works in your environment. Possible bug in Mojarra 2.1.7 perhaps. – LightGuard Nov 15 '12 at 16:01
  • 1
    @LightGuard Running this on MyFaces produced the same results, so I doubt this is a bug in the JSF implementation. I looked into this further - please see the edit in the original question above. This leads me to believe that even the CODI implementation is flawed; i.e. both my example and the CODI implementations ensure that there is a 1-to-1 relationship between a view instance and an instance of bean type, but fail to properly destroy the instances. I wonder if it's worth bringing this up to the CODI developers. – rdcrng Nov 16 '12 at 01:08
  • Anyone from CODI reading this? The class in discussion is `org.apache.myfaces.extensions.cdi.jsf2.impl.scope.view.ViewScopedContext`. – rdcrng Nov 16 '12 at 01:10
  • We're working on this in DeltaSpike now. Posting on the user or dev list would be a great place to make sure we get it right this time. – LightGuard Nov 16 '12 at 06:24

2 Answers2

1

This is related to an issue with the JSF spec that is being fixed in the JSF2.2 spec, see here. Also, I created an issue with Apache DeltaSpike so they may try to fix it, see here. If it's fixed in DeltaSpike, then it may end up being merged into CODI and / or Seam as well.

rdcrng
  • 3,415
  • 2
  • 18
  • 36
0

The view map is stored in a LRU map, because you never know which view will be post back. Unfortunately, the PreDestroyViewMapEvent is not called before removing from this map.

A workaround is to reference your object from within a WeakReference. You can use ReferenceQueue or check the reference when to call your destruction code.

Tires
  • 1,529
  • 16
  • 27
  • 1
    FYI: rdcrng's contribution eventually ended up in OmniFaces since version 1.6, which is also further polished by me. The LRU map being used by OmniFaces supports firing a listener on eviction, so that case is also covered. See also http://showcase.omnifaces.org/cdi/ViewScoped for example and links to docs/sources. – BalusC Nov 25 '13 at 14:12
  • @Tires Indeed as BalusC points out, with the help of Omnifaces community, this ended up as a nice feature of Omnifaces - a great implementation which works across many app servers and even in clustered environments. As for your idea of using weak references - that was my first approach as well, however the DeltaSpike team didn't seem to like it, see http://mail-archives.apache.org/mod_mbox/incubator-deltaspike-dev/201211.mbox/%3cCALqnCbik+3nO5kqzmCar-ywzNBuHu686A0HZVO-rR-ac5_NREA@mail.gmail.com%3e. – rdcrng Nov 27 '13 at 06:39