Search code examples
jsfjsf-2primefacescomposite-component

Is it possible to call a method defined in method-signature programmatically?


Suppose I have a composite component

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:composite="http://java.sun.com/jsf/composite"
  `enter code here`    xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui">

<h:head></h:head>
<h:body>
    <composite:interface componentType="editableLabel">
        <composite:attribute name="value" required="true" type="java.lang.String"/>
        <composite:attribute name="oldValue" type="java.lang.String"/>
        <composite:attribute name="editMode" required="false" default="#{false}" type="java.lang.Boolean"/>
        <composite:attribute name="updateListener" required="false" method-signature="void actionListener()"/>
        <composite:attribute name="cancelListener" required="false" method-signature="void actionListener()"/>
    </composite:interface>
    <composite:implementation>
        <h:panelGroup id="editableLabelComponent">
            <h:panelGroup rendered="#{cc.attrs.editMode}">
                <p:inputText value="#{cc.attrs.value}"/>

                <p:commandButton value="Update" actionListener="#{cc.update}" update="editableLabelComponent"/>

                <p:commandButton value="Cancel" actionListener="#{cc.cancel}" update="editableLabelComponent"/>
            </h:panelGroup>

            <p:commandLink>
                <p:outputLabel id="display" value="#{cc.attrs.value}" rendered="#{!cc.attrs.editMode}"/>
                <p:ajax event="click" listener="#{cc.toggleEditMode}" update="editableLabelComponent"/>
            </p:commandLink>
        </h:panelGroup>
    </composite:implementation>
</h:body>

</html>

with a FacesComponent

import org.apache.commons.lang3.StringUtils;

import javax.el.ELContext;
import javax.el.ValueExpression;
import javax.faces.component.FacesComponent;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponentBase;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
import java.io.Serializable;

/**
 * Created by labraham on 1/14/16.
 */
@FacesComponent(value = "editableLabel")
public class EditableLabel extends UIComponentBase implements Serializable, NamingContainer {
  private static final long serialVersionUID = 108467781935083432L;

  public static final String VALUE_ATTRIBUTE = "#{cc.attrs.value}";
  public static final String OLD_VALUE_ATTRIBUTE = "#{cc.attrs.oldValue}";
  public static final String EDIT_MODE_ATTRIBUTE = "#{cc.attrs.editMode}";

  private FacesContext context;
  private ELContext elContext;

  @Override
  public String getFamily() {
    return UINamingContainer.COMPONENT_FAMILY;
  }

  /**
   *
   */
  public void update() {

    ValueExpression oldValue = getAttr(OLD_VALUE_ATTRIBUTE, String.class);

    String value = (String) getAttr(VALUE_ATTRIBUTE, String.class).getValue(getElContext());

    oldValue.setValue(getElContext(), value);

    toggleEditMode();
  }

  /**
   *
   */
  public void cancel() {
    ValueExpression value = getAttr(VALUE_ATTRIBUTE, String.class);

    String oldValue = (String) getAttr(OLD_VALUE_ATTRIBUTE, String.class).getValue(getElContext());

    value.setValue(getElContext(), oldValue);

    toggleEditMode();
  }

  /**
   *
   */
  public void toggleEditMode() {
    if (StringUtils.isEmpty((String) getAttr(OLD_VALUE_ATTRIBUTE, String.class).getValue(getElContext()))) {
     getAttr(OLD_VALUE_ATTRIBUTE, String.class).setValue(getElContext(),
         getAttr(VALUE_ATTRIBUTE, String.class).getValue(getElContext())
     );
    }

    ValueExpression editmode = getAttr(EDIT_MODE_ATTRIBUTE, Boolean.class);
    editmode.setValue(getElContext(), !((Boolean) editmode.getValue(getElContext())));

  }

  /**Get value expression for a given attr.*/
  private ValueExpression getAttr(final String attribute, final Class<?> attributeType) {
    return getContext().getApplication().getExpressionFactory()
           .createValueExpression(getElContext(), attribute, attributeType);
  }

  /**Get ElContext.*/
  public ELContext getElContext() {
    if (this.elContext == null) {
      elContext = getContext().getELContext();
    }
    return elContext;
  }

  public void setElContext(final ELContext elContext) {
    this.elContext = elContext;
  }

  /**Get faces context*/
  public FacesContext getContext() {
    if (this.context == null) {
      context = FacesContext.getCurrentInstance();
    }
    return context;
  }

  public void setContext(final FacesContext context) {
    this.context = context;
  }
}

now all instances of this widget will have the baseline functionality for cancel and update as defined by the cancel() and update() functions. However in addition this basic functionality the user may want some additional functionality (for instance calling a webservice on update) which they will add by some sort of backing bean method in the updateListener and/or cancelListener attributes.

My question is Is it possible to call the method that the user attached to the listener attributes programmatically from inside the FacesComponent or am I going about this the wrong way?


Solution

  • Yes, it's available as a MethodExpression typed attribute. All composite attributes are available the usual way by the inherited UIComponent#getAttributes() map. See also How to access Composite Component attribute values in the backing UIComponent?

    E.g., to get #{cc.attrs.updateListener} and invoke with no params:

    MethodExpression updateListener = (MethodExpression) getAttributes().get("updateListener");
    updateListener.invoke(getFacesContext().getELContext(), null);
    

    Unrelated to the concrete problem, the way how you deal with ValueExpression attributes is clumsy. To get #{cc.attrs.oldvalue}, just do:

    String oldvalue = (String) getAttributes().get("oldvalue");
    

    To set #{cc.attrs.value}, just do:

    getAttributes().put("value", oldvalue);
    

    Having FacesContext and ELContext as instance variables does technically fortunately not harm in this specific construct, but this is fishy, you'd better get them in method local scope. The FacesContext is just available by inherited UIComponent#getFacesContext().

    See also: