Friday, July 1, 2011

Auto updatable JSF components are easy done

Do you want to update automatically any JSF components with each ajax response? Are you looking for a dynamic way without needs to touch and change components? Nothing is easier than that. Auto updatable components make sense. They could be messages, tooltips, or everything else what is avalaible on every page and have to be updated. An own PartialViewContext and JSF system events on the component level will help us to archieve this aim. We start with a listener which must be fired on the PreRenderComponentEvent. Components to be auto updated are added there to the view map of the view root. More precisely - client IDs of such components will be added shortly before components get rendered.
package com.xyz.webapp.jsf

import ...

public class AutoUpdatableComponentListener implements SystemEventListener {
    public static final String AUTO_UPDATED_IDS = "com.xyz.webapp.jsf.AutoUpdatedIds";

    @SuppressWarnings("unchecked")
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        FacesContext fc = FacesContext.getCurrentInstance();
        if (fc.getPartialViewContext().isAjaxRequest()) {
            // auto updatable component was already added to the view map
            return;
        }

        UIComponent component = (UIComponent) event.getSource();
        Map<String, Object> viewMap = fc.getViewRoot().getViewMap();
        Collection<String> autoUpdatedIds = (Collection<String>) fc.getViewRoot().getViewMap().get(AUTO_UPDATED_IDS);

        if (autoUpdatedIds == null) {
            autoUpdatedIds = new HashSet<String>();
        }

        autoUpdatedIds.add(component.getClientId());
        viewMap.put(AUTO_UPDATED_IDS, autoUpdatedIds);
    }

    public boolean isListenerForSource(Object source) {
        return true;
    }
}
The view map is accessed in a special implementaion of the PartialViewContext where all client IDs of auto updatable components have to be added to IDs getting from getRenderIds().
package com.xyz.webapp.jsf.context;

import com.xyz.webapp.jsf.AutoUpdatableComponentListener;
import javax.faces.context.FacesContext;
import javax.faces.context.PartialViewContext;
import javax.faces.context.PartialViewContextWrapper;
import javax.faces.event.PhaseId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class MyPartialViewContext extends PartialViewContextWrapper {
    private PartialViewContext wrapped;

    public MyPartialViewContext(PartialViewContext wrapped) {
        this.wrapped = wrapped;
    }

    public PartialViewContext getWrapped() {
        return this.wrapped;
    }

    public void setPartialRequest(boolean isPartialRequest) {
        getWrapped().setPartialRequest(isPartialRequest);
    }

    @SuppressWarnings("unchecked")
    public Collection<String> getRenderIds() {
        FacesContext fc = FacesContext.getCurrentInstance();
        if (PhaseId.RENDER_RESPONSE.compareTo(fc.getCurrentPhaseId()) != 0) {
            return getWrapped().getRenderIds();
        } else {
            List<String> ids = new ArrayList<String>(getWrapped().getRenderIds());
            Collection<String> autoUpdatedIds = (Collection<String>) FacesContext.getCurrentInstance().
                getViewRoot().getViewMap().get(AutoUpdatableComponentListener.AUTO_UPDATED_IDS);

            if (autoUpdatedIds != null) {
                ids.addAll(autoUpdatedIds);
            }

            return ids;
        }
    }
}

package com.xyz.webapp.jsf.context;

import javax.faces.context.FacesContext;
import javax.faces.context.PartialViewContext;
import javax.faces.context.PartialViewContextFactory;

public class MyPartialViewContextFactory extends PartialViewContextFactory {
    private PartialViewContextFactory parent;

    public MyPartialViewContextFactory(PartialViewContextFactory parent) {
        this.parent = parent;
    }

    @Override
    public PartialViewContextFactory getWrapped() {
        return this.parent;
    }

    @Override
    public PartialViewContext getPartialViewContext(FacesContext fc) {
        return new MyPartialViewContext(getWrapped().getPartialViewContext(fc));
    }
}
For MyPartialViewContext is important to know that getRenderIds() is called in four JSF lifecycle phases. We are only interesting in the "render response" phase. That is why I made this check
if (PhaseId.RENDER_RESPONSE.compareTo(fc.getCurrentPhaseId()) != 0) {
    return getWrapped().getRenderIds();
}
The last step - register all stuff in the faces-config.xml.
<factory>
    <partial-view-context-factory>
        com.xyz.webapp.jsf.context.MyPartialViewContextFactory
    </partial-view-context-factory>
</factory>

<system-event-listener>
    <system-event-listener-class>com.xyz.webapp.jsf.AutoUpdatableComponentListener</system-event-listener-class>
    <system-event-class>javax.faces.event.PreRenderComponentEvent</system-event-class>
    <source-class>com.xyz.webapp.jsf.component.MyComponent</source-class>
</system-event-listener>
I have registered the listener for com.xyz.webapp.jsf.component.MyComponent, but we can made the configuration a little bit more flexible via a config parameter in web.xml.
<context-param>
    <param-name>AUTO_UPDATABLE_COMPONENTS</param-name>
    <param-value>
        com.xyz.webapp.jsf.component.MyComponent,
        javax.faces.component.html.HtmlMessage,
        org.primefaces.component.tooltip.Tooltip
    </param-value>
</context-param>
We have no needs then to specify source-class and the isListenerForSource() method looks like
// This is just an example. Evaluation of init parameters should be done outside of the isListenerForSource() of course.
public boolean isListenerForSource(Object source) {
    String components = 
        FacesContext.getCurrentInstance().getExternalContext().getInitParameter("AUTO_UPDATABLE_COMPONENTS");
    if (components == null) {
        return false;
    }

    String[] arr = components.split("[,\\s]+");
    for (String component : arr) {
        if (component.equals(source.getClass().getName())) {
            return true;
        }
    }

    return false;
}
We can go on and configure components client IDs instead of their fully qualified names (better in my opinion). Check against IDs happens in the isListenerForSource() too. More flexible and precise configurations are possible - e.g. client IDs + pages (= view root IDs) where auto updatable components are placed exactly.

2 comments:

  1. Hello Oleg,
    Nice article, I think, I too have little bit same requirement like this. In my case, I have one richfaces popupPanel component whose ID will be generate at run time. But in jsp, I have to mention something like, "xyz#{somebean.attribute}". And the page in which this component is, also decided dynamically. So, I have to add this component dynamically in the tree of component tree of the page. Could you please guide me ho to get this.

    Thanks
    Jaikrat Singh

    ReplyDelete
  2. Very helpful article, thank you!

    ReplyDelete

Note: Only a member of this blog may post a comment.