ViewMapListener JSF not called

I am trying to pass JSF @ViewScoped annotation to CDI. The reason is that it is educational rather than necessity-based. I chose this particular scale mainly due to the lack of a better concrete example of a user area that could be implemented in CDI.

However, my starting point was Porting JSF @ViewScoped annotations to CDI . But this implementation does not seem to take into account the very important responsibility of the Context (i.e. destruction) mentioned in the API :

A context object is responsible for creating and destroying context instances by invoking Contextual operations. In particular, a context object is responsible for destroying any context instance that it creates by passing an instance of Contextual.destroy (Object, CreationalContext). An attempt to destroy an instance cannot be returned by the get () function. The context object should pass the same instance of CreationalContext to Contextual.destroy () that it passed Contextual.create () when it instantiated.

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

  • keep track of which Contextual objects it creates for which UIViewRoot s;
  • we implement the ViewMapListener interface and register ourselves as a listener for each UIViewRoot , calling UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this) ;
  • destroy any Contextual created when ViewMapListener.processEvent(SystemEvent event) is called and ViewMapListener.processEvent(SystemEvent event) it from UIViewRoot .

Here is my implementation of Context :

 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 area 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 to META-INF/services containing com.example.CustomContextsExtension to correctly register above with CDI.

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

 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 { ... } 

beans are created correctly and inserted correctly in JSF pages (i.e., one instance is returned for each view, new ones are created only when the view is created, the same instances are entered for several requests for the same view), How do I know? Imagine the code above is littered with debugging code that I purposely deleted for clarity, and since this is already a huge post.

The problem is that my ViewContext.isListenerForSource(Object source) and ViewContext.processEvent(SystemEvent event) never called. I expected that at least after the session expires these calls will be called since the view map is stored on the session map (right?). I set the session timeout to 1 minute, waited, saw the timeout, but my listener was still not called.

I also tried adding the following to my faces-config.xml (mainly due to 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 JBoss AS 7.1.1 environment with Mojarra 2.1.7 .

Any hints are welcome.

EDIT: Further research.

PreDestroyViewMapEvent does not work at all, and PostConstructViewMapEvent starts, as expected, every time a new view map is created, namely UIViewRoot.getViewMap(true) . The documentation states that PreDestroyViewMapEvent should be run every time clear() is called on the view map. This leaves one wondering if clear() is required at all? If so, when?

The only place in the documentation where I could find such a requirement is in FacesContext.setViewRoot() :

If the current UIViewRoot is not null and calls equals () on the root argument, passing the current UIViewRoot returns false, a clear method should be called on the map returned from UIViewRoot # getViewMap.

Does this UIViewRoot.setViewMap() in the normal JSF life cycle, that is, without a programmatic call to UIViewRoot.setViewMap() ? I do not seem to see any indication.

+4
source share
2 answers

This is due to a problem with the JSF specification, which is fixed in the JSF2.2 specification, see here . Also, I created a problem with Apache DeltaSpike so that they can fix it, see here . If it is installed in DeltaSpike, then it can also be combined with CODI and / or Seam.

+1
source

The view map is stored on the LRU map because you never know which view will be sent back. Unfortunately, PreDestroyViewMapEvent is not called before being removed from this map.

The workaround is to reference your object using the WeakReference method. You can use the ReferenceQueue or check the link when calling the destruction code.

0
source

Source: https://habr.com/ru/post/1445930/


All Articles