Search code examples
jsfjakarta-eecdiapache-tomeejsf-2.3

JSF 2.3 Custom converter with generics


We now started to use JSF 2.3 on our existing JSF 2.2 project. On our custom converters we got warning Converter is a raw type. References to generic type Converter<T> should be parameterized. Problem that we experiencing is when we tried to fix that warning using generics:

@FacesConverter(value = "myConverter", managed = true)
public class MyConverter implements Converter<MyCustomObject>{  

@Override
public MyCustomObject getAsObject(FacesContext context, UIComponent component, String submittedValue){}

@Override
public String getAsString(FacesContext context, UIComponent component, MyCustomObject modelValue) {}
}

and when converter is used for example in

<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:selectOneMenu id="#{componentId}" value="#{componentValue}">
    <f:converter converterId="myConverter" />
    <f:selectItem itemLabel="label"
        itemValue="" />
    <f:selectItems value="listOfValues"
        var="singleValue"
        itemValue="singleValue.value"
        itemLabel="singleValue.label" />
</h:selectOneMenu>

then ClassCastException with message java.lang.String cannot be cast to MyCustomObjectis thrown. There is also one line in stacktrace that maybe can help com.sun.faces.cdi.CdiConverter.getAsString(CdiConverter.java:109).

But when converter generics definition changed from MyCustomObject to Object :

@FacesConverter(value = "myConverter", managed = true)    
public class MyConverter implements Converter<Object>{  

@Override
public Object getAsObject(FacesContext context, UIComponent component, String submittedValue){}

@Override
public String getAsString(FacesContext context, UIComponent component, Object modelValue) {}
}

then everything works as expected, but that obviously beats the purpose of Converter<T> interface.


Solution

  • I had same issue here and came up with following solution that actually not just compiles but also runs well:

    some_page.xhtml (relevant excerpt):

    <h:selectOneMenu styleClass="select" id="companyUserOwner" value="#{adminCompanyDataController.companyUserOwner}">
        <f:converter converterId="UserConverter" />
        <f:selectItem itemValue="#{null}" itemLabel="#{msg.NONE_SELECTED}" />
        <f:selectItems value="#{userController.allUsers()}" var="companyUserOwner" itemValue="#{companyUserOwner}" itemLabel="#{companyUserOwner.userContact.contactFirstName} #{companyUserOwner.userContact.contactFamilyName} (#{companyUserOwner.userName})" />
    </h:selectOneMenu>
    

    Please note that the above JSF code is full of custom stuff and may not work on your end. And that my converter compiles without rawtype warning (JSF 2.3+, not 2.2!):

    SomeUserConverter.java:

    @FacesConverter (value = "UserConverter")
    public class SomeUserConverter implements Converter<User> {
    
        /**
         * User EJB
         */
        private static UserSessionBeanRemote USER_BEAN;
    
        /**
         * Default constructor
         */
        public SomeUserConverter () {
        }
    
        @Override
        public User getAsObject (final FacesContext context, final UIComponent component, final String submittedValue) {
            // Is the value null or empty?
            if ((null == submittedValue) || (submittedValue.trim().isEmpty())) {
                // Return null
                return null;
            }
    
            // Init instance
            User user = null;
    
            try {
                // Try to parse the value as long
                final Long userId = Long.valueOf(submittedValue);
    
                // Try to get user instance from it
                user = USER_BEAN.findUserById(userId);
            } catch (final NumberFormatException ex) {
                // Throw again
                throw new ConverterException(ex);
            } catch (final UserNotFoundException ex) {
                // User was not found, return null
            }
    
            // Return it
            return user;
        }
    
        @Override
        public String getAsString (final FacesContext context, final UIComponent component, final User value) {
            // Is the object null?
            if ((null == value) || (String.valueOf(value).isEmpty())) {
                // Is null
                return ""; //NOI18N
            }
    
            // Return id number
            return String.valueOf(value.getUserId());
        }
    
    }
    

    This converter class has the JNDI lookup removed (I will rewrite that part later anyway) but it should be enough for demonstrating the important parts:

    • Converter<User> (by User is a custom POJI) preventing raw-type warning
    • value="UserConverter" that is the actual name you use in your JSF page
    • And most important, which actually fixed the ClassCastException:
    • <f:selectItem itemValue="#{null}" itemLabel="#{msg.NONE_SELECTED}" />
    • By omitting #{null} an empty string instead of null is being handled over to your getAsString method!

    This last one actually fixed the said exception and my application is working again with much lesser warnings and much better type-hinting.

    Need to remove forClass as value is there: FacesConverter is using both value andforClass, only value will be applied.

    The raw-type warning will come (since Java SE 5) because you use an interface which has a generic (mostly stated as <T> after the interface name) which you need to satisfy so the compiler stop throwing that warning at you.