JSF: auto conversion when backing a view item with an instance of a subclass of a generic class with resolved type parameter

StackOverflow https://stackoverflow.com/questions/17055660

Pergunta

We have a hierarchy of generic "parameter" types, with non-generic concrete leaf classes, a vertical slice of it given here:

public interface Parameter<T>
  public T getValue();
  public void setValue(T value);
  …

public abstract class AbstractParameter<T> implements Parameter<T>
  public final T getValue()
  public final void setValue(T value)
  …

public abstract class AbstractNumberParameter<T extends Number> extends AbstractParameter<T> implements NumberParameter<T>
  …

public class IntegerParameter extends AbstractNumberParameter<Integer>
  …

getValue and setValue are not overridden in AbstractNumberParameter or IntegerParameter.

In one case, an instance of IntegerParameter is used in an iteration of facelet code that iterates through a parameter list and conditionally renders a parameter's corresponding view depending on the value of an enum field in the parameter (which is more coarse than the generic type parameter).

<p:dataTable id="inputPanel" styleClass="parameter-input-table"
                value="#{metricModuleManager.metricModule.parameters}"
                var="parameter" rowIndexVar="rowIndex">

    ...

    <p:column>
        <h:panelGroup id="inputArea">

        ...

        <p:spinner id="numberInput"
            rendered="#{parameter.type=='NUMBER'}"
            stepFactor="#{parameter.resolution}"
            value="#{parameter.value}"
            parameter="#{parameter.name}"
            validator="#{metricModuleManager.validateParameter}">
            <p:ajax update="numberMessage" />
            <p:message id="numberMessage" for="numberInput" />
        </p:spinner>

        ...

        </h:panelGroup>
    </p:column>
    ...

</p:dataTable>

(PrimeFaces elements are used.)

It seems to vary between development or runtime environments whether or not the value of the spinner is correctly converted to the setter parameter's type. To avoid a later ClassCastException, the value being passed to setValue would need to be an Integer instance. In my case, I am observing a String being passed to the setter (String apparently being the default type for attributes, if I understand correctly). However, the same code seems to work perfectly on my coworker's computer, such that the integer value is used in the correct spot later on in the workflow. It also seems to work correctly on a separate server.

I and my coworker are both using JSF 2 with Tomcat 7 integrated with Eclipse.

If this code consistently didn't work, then I would just assume it was related to type erasure. But, the inconsistent behavior makes me think that perhaps at least some implementations of JSF are able to draw from limited type metadata in this case to determine the correct converter to use. It does seem like the actual Integer type argument is stored in IntegerParameter's Class metadata:

p.getClass().getGenericSuperclass().toString() yielded AbstractNumberParameter<Integer> in debug code where p was the IntegerParameter instance.

ClassCastException when calling TreeSet<Long>.contains( Long.valueOf( someLongValue ) ) and https://stackoverflow.com/a/13866179/516433 establish that through type erasure, a direct instance of a class with a generic type parameter won't carry with it the actual type argument, so in such a case, the JSF implementation wouldn't have the needed type information to choose the correct converter. But, is this case different in that the type argument is specified as part of the class definition? (https://stackoverflow.com/a/16886880/1867423 suggests this is the case.) Would the JSF implementation then be able to use introspection to determine the proper converter to use?

What might explain the difference in behavior between environments? We are somewhat stumped.

Foi útil?

Solução

I've done something similar a couple months ago addressing a different problem. I have an abstract class for many of my JPA entity classes. Every entity converter of that type had almost the same logic and I wanted to use a single converter for that.

After a little research I was able to find a solution for an abstract converter for my entities where I could place the common logic. The only problem is that now I need to reference-it by its name (converter="entity"). This solution was found somewhere else (I can't remember where) so I can't take any credits here.

The abstract converter:

import java.util.HashMap;
import java.util.Map;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public abstract class AbstractConverter implements Converter {

    private static final String key = "any.key.you.want";

    protected Map<String, Object> getViewMap(FacesContext context) {
        Map<String, Object> viewMap = context.getViewRoot().getViewMap();
        Map<String, Object> idMap = (Map) viewMap.get(key);
        if (idMap == null) {
            idMap = new HashMap<String, Object>();
            viewMap.put(key, idMap);
        }
        return idMap;
    }

    @Override
    public final Object getAsObject(FacesContext context, UIComponent c,String value) {
        if (value == null || value.isEmpty()) {
            return null;
        }
        return getViewMap(context).get(value);
    }

    @Override
    public final String getAsString(FacesContext context, UIComponent c, Object value) {
        if (value != null) {
            String id = getConversionId(value);
            if (id == null || id.isEmpty()) {
                throw new IllegalArgumentException("Object can't be converted.");
            }
            getViewMap(context).put(id, value);
            return id;
        }
        return null;
    }

    public abstract String getConversionId(Object value);
}

The concrete converter:

import javax.faces.convert.FacesConverter;
import entity.AbstractEntity;

@FacesConverter("entity")
public class EntityConverter extends AbstractConverter {

    @Override
    public String getConversionId(Object value) {

        if (value instanceof AbstractEntity) {
            AbstractEntity entity = (AbstractEntity) value;
            StringBuilder sb = new StringBuilder();
            sb.append(entity.getClass().getSimpleName());
            sb.append("@");
            sb.append(entity.getId());
            return sb.toString();
        }

        return null;
    }
}

Assuming that this could solve you problem you would need something like a ParameterConverter annotated with @FacesConverter("parameter") and then use it as follows:

<p:spinner id="numberInput"
        rendered="#{parameter.type=='NUMBER'}"
        stepFactor="#{parameter.resolution}"
        value="#{parameter.value}"
        parameter="#{parameter.name}"
        validator="#{metricModuleManager.validateParameter}"
        converter="parameter"
        >
        <p:ajax update="numberMessage" />
        <p:message id="numberMessage" for="numberInput" />
    </p:spinner>
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top