8

I'm working on JSF project with Spring and Hibernate which among other things has a number of Converters 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 id

  • getAsString receives and entity and returns the id of the object converted to String

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 Converters 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 ?

3 Answers 3

17

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.

2
  • 2
    Using the "id" property is not recommended in equals and hashCode methods: They recommend implementing equals() and hashCode() using Business key equality: docs.jboss.org/hibernate/stable/core.old/reference/en/html/… And how about composite ids? Commented Sep 26, 2015 at 17:23
  • That's correct. But these don't belong in the BaseEntity. You can just override equals/hashCode in the particular entity subclass. It's impossible and ugly to put all possible cases directly in the BaseEntity.
    – BalusC
    Commented Nov 22, 2018 at 10:44
1

Your entities don't need to inherit from a BaseEntity as the EntityManagerFactory contains all the necessary (meta) information. You can also reuse JSF Converters to convert/parse the id.

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

    @Inject
    private EntityManager entityManager;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        Class<?> entityType = component.getValueExpression("value").getType(context.getELContext());
        Class<?> idType = entityManager.getMetamodel().entity(entityType).getIdType().getJavaType();
        Converter idConverter = context.getApplication().createConverter(idType);
        Object id = idConverter.getAsObject(context, component, value);
        return entityManager.getReference(entityType, id);
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        Object id = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(value);
        Converter idConverter = context.getApplication().createConverter(id.getClass());
        return idConverter.getAsString(context, component, id);
    }
}
0

Here is my solution with this considerations:

  • I asume you are interested in JPA (not Hibernate)
  • My solution does not require to extends any class and should work for any JPA entity bean, it's just only a simple class you use, nor does it require implementing any service or DAO. The only requirement is that the converter directly depends on the JPA library which may be not very elegant.
  • It uses auxiliary methods for serializing/deserializing the id of the bean. It only converts the id of the entity bean and compounds the string with the classname and the id serialized and converted to base64. This is possible due to the fact that in jpa the ids of the entities must implement serializable. The implementation of this methods is in java 1.7, but you could find another implementations for java < 1.7 over there
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.persistence.EntityManagerFactory;

/**
 * Generic converter of jpa entities for jsf
 * 
 * Converts the jpa instances to strings with this form: @ Converts from strings to instances searching by id in
 * database
 * 
 * It is possible thanks to the fact that jpa requires all entity ids to
 * implement serializable
 * 
 * Requires: - You must provide instance with name "entityManagerFactory" to be
 * injected - Remember to implement equals and hashCode in all your entity
 * classes !!
 * 
 */
@ManagedBean
@RequestScoped
public class EntityConverter implements Converter {

    private static final char CHARACTER_SEPARATOR = '@';

    @ManagedProperty(value = "#{entityManagerFactory}")
    private EntityManagerFactory entityManagerFactory;

    public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) {
        this.entityManagerFactory = entityManagerFactory;
    }

    private static final String empty = "";

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

        int index = value.indexOf(CHARACTER_SEPARATOR);
        String clazz = value.substring(0, index);
        String idBase64String = value.substring(index + 1, value.length());
EntityManager entityManager=null;
        try {
            Class entityClazz = Class.forName(clazz);
            Object id = convertFromBase64String(idBase64String);

        entityManager = entityManagerFactory.createEntityManager();
        Object object = entityManager.find(entityClazz, id);

            return object;

        } catch (ClassNotFoundException e) {
            throw new ConverterException("Jpa entity not found " + clazz, e);
        } catch (IOException e) {
            throw new ConverterException("Could not deserialize id of jpa class " + clazz, e);
        }finally{
        if(entityManager!=null){
            entityManager.close();  
        }
    }

    }

    @Override
    public String getAsString(FacesContext context, UIComponent c, Object value) {
        if (value == null) {
            return empty;
        }
        String clazz = value.getClass().getName();
        String idBase64String;
        try {
            idBase64String = convertToBase64String(entityManagerFactory.getPersistenceUnitUtil().getIdentifier(value));
        } catch (IOException e) {
            throw new ConverterException("Could not serialize id for the class " + clazz, e);
        }

        return clazz + CHARACTER_SEPARATOR + idBase64String;
    }

    // UTILITY METHODS, (Could be refactored moving it to another place)

    public static String convertToBase64String(Object o) throws IOException {
        return javax.xml.bind.DatatypeConverter.printBase64Binary(convertToBytes(o));
    }

    public static Object convertFromBase64String(String str) throws IOException, ClassNotFoundException {
        return convertFromBytes(javax.xml.bind.DatatypeConverter.parseBase64Binary(str));
    }

    public static byte[] convertToBytes(Object object) throws IOException {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput out = new ObjectOutputStream(bos)) {
            out.writeObject(object);
            return bos.toByteArray();
        }
    }

    public static Object convertFromBytes(byte[] bytes) throws IOException, ClassNotFoundException {
        try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInput in = new ObjectInputStream(bis)) {
            return in.readObject();
        }
    }

}

Use it like another converter with

<h:selectOneMenu converter="#{entityConverter}" ...

Not the answer you're looking for? Browse other questions tagged or ask your own question.