Implement converters for entities with Java Generics
I'm working on JSF project with Spring and Hibernate which among other things has a number of Converter
s that follow the same pattern:
getAsObject
receives the string representation of the object id, converts it to a number, and fetch the entity of the given kind and the given idgetAsString
receives and entity and returns the id of the object converted toString
The code is essentially what follows (checks omitted):
@ManagedBean(name="myConverter")
@SessionScoped
public class MyConverter implements Converter {
private MyService myService;
/* ... */
@Override
public Object getAsObject(FacesContext facesContext, UIComponent uiComponent, String value) {
int id = Integer.parseInt(value);
return myService.getById(id);
}
@Override
public String getAsString(FacesContext facesContext, UIComponent uiComponent, Object value) {
return ((MyEntity)value).getId().toString();
}
}
Given the large number of Converter
s that are exactly like this (except for the type of MyService
and MyEntity
of course), I was wondering if it was worth using a single generic converter.
The implementation of the generic by itself is not difficult, but I'm not sure about the right approach to declare the Beans.
A possible solution is the following:
1 - Write the generic implementation, let's call it MyGenericConverter
, without any Bean annotation
2 - Write the specific converter ad a subclass of MyGenericConverter<T>
and annotate it as needed:
@ManagedBean(name="myFooConverter")
@SessionScoped
public class MyFooConverter implements MyGenericConverter<Foo> {
/* ... */
}
While writing this I realized that maybe a Generic is not really needed, so maybe I could simply write a base class with the implementation of the two methods, and subclass as needed.
There a few non trivial details that have to be taken care of (like the fact that I'd have to abstract the MyService
class in some way) so my first question is : is it worth the hassle ?
And if so, are there other approaches ?
Solution 1:
Easiest would be to let all your JPA entities extend from a base entity like this:
public abstract class BaseEntity<T extends Number> implements Serializable {
private static final long serialVersionUID = 1L;
public abstract T getId();
public abstract void setId(T id);
@Override
public int hashCode() {
return (getId() != null)
? (getClass().getSimpleName().hashCode() + getId().hashCode())
: super.hashCode();
}
@Override
public boolean equals(Object other) {
return (other != null && getId() != null
&& other.getClass().isAssignableFrom(getClass())
&& getClass().isAssignableFrom(other.getClass()))
? getId().equals(((BaseEntity<?>) other).getId())
: (other == this);
}
@Override
public String toString() {
return String.format("%s[id=%d]", getClass().getSimpleName(), getId());
}
}
Note that it's important to have a proper equals()
(and hashCode()
), otherwise you will face Validation Error: Value is not valid. The Class#isAssignableFrom()
tests are to avoid failing tests on e.g. Hibernate based proxies without the need to fall back to Hibernate-specific Hibernate#getClass(Object)
helper method.
And have a base service like this (yes, I'm ignoring the fact that you're using Spring; it's just to give the base idea):
@Stateless
public class BaseService {
@PersistenceContext
private EntityManager em;
public BaseEntity<? extends Number> find(Class<BaseEntity<? extends Number>> type, Number id) {
return em.find(type, id);
}
}
And implement the converter as follows:
@ManagedBean
@ApplicationScoped
@SuppressWarnings({ "rawtypes", "unchecked" }) // We don't care about BaseEntity's actual type here.
public class BaseEntityConverter implements Converter {
@EJB
private BaseService baseService;
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
if (value == null) {
return "";
}
if (modelValue instanceof BaseEntity) {
Number id = ((BaseEntity) modelValue).getId();
return (id != null) ? id.toString() : null;
} else {
throw new ConverterException(new FacesMessage(String.format("%s is not a valid User", modelValue)), e);
}
}
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
Class<?> type = component.getValueExpression("value").getType(context.getELContext());
return baseService.find((Class<BaseEntity<? extends Number>>) type, Long.valueOf(submittedValue));
} catch (NumberFormatException e) {
throw new ConverterException(new FacesMessage(String.format("%s is not a valid ID of BaseEntity", submittedValue)), e);
}
}
}
Note that it's registered as a @ManagedBean
instead of a @FacesConverter
. This trick allows you to inject a service in the converter via e.g. @EJB
. See also How to inject @EJB, @PersistenceContext, @Inject, @Autowired, etc in @FacesConverter? So you need to reference it as converter="#{baseEntityConverter}"
instead of converter="baseEntityConverter"
.
If you happen to use such a converter more than often for UISelectOne
/UISelectMany
components (<h:selectOneMenu>
and friends), you may find OmniFaces SelectItemsConverter
much more useful. It converts based on the values available in <f:selectItems>
instead of making (potentially expensive) DB calls everytime.