2013-06-27 7 views
6

Sto lavorando su progetto JSF con la Primavera e Hibernate che tra l'altro ha un certo numero di Converter s che seguono lo stesso schema:Implementare convertitori per gli enti con Java Generics

  • getAsObject riceve la rappresentazione di stringa di l'oggetto id, lo converte in un numero, e recuperare l'entità del dato genere e l'id proposta

  • getAsString riceve ed entità e restituisce l'id dell'oggetto convertito String

Il codice è essenzialmente quanto segue (controlli omessi):

@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(); 
    } 
} 

dato il gran numero di Converter s che sono esattamente simili (ad eccezione del tipo di MyService e MyEntity ovviamente), ero chiedendo se valesse la pena usare un singolo convertitore generico. L'implementazione del generico di per sé non è difficile, ma non sono sicuro dell'approccio giusto per dichiarare i bean.

Una possibile soluzione è la seguente:

1 - Scrivere l'implementazione generica, chiamiamolo MyGenericConverter, senza alcuna annotazione Bean

2 - Scrivere la specifica annuncio convertitore una sottoclasse di MyGenericConverter<T> e annotare come necessario:

@ManagedBean(name="myFooConverter") 
@SessionScoped 
public class MyFooConverter implements MyGenericConverter<Foo> { 
    /* ... */ 
} 

Mentre scrivevo questo mi sono reso conto che forse un generico non è realmente necessario, quindi forse si potrebbe semplicemente scrivere una classe base con l'attuazione dei due metodi, uno d sottoclasse secondo necessità.

Ci sono alcuni dettagli non banali che devono essere risolti (come il fatto che dovrei astrarre la classe MyService in qualche modo) quindi la mia prima domanda è: vale la pena?

E se sì, ci sono altri approcci?

risposta

15

più semplice sarebbe quella di lasciare che tutti i vostri entità JPA si estendono da un'entità di base come questo:

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()); 
    } 

} 

Si noti che è importante avere una corretta equals() (e hashCode()), altrimenti si troveranno ad affrontare Validation Error: Value is not valid. I test Class#isAssignableFrom() servono per evitare test falliti su es. I proxy basati su Hibernate non richiedono il ricorso al metodo helper Hibernate-specifico Hibernate#getClass(Object).

E hanno un servizio di base come questo (sì, sto ignorando il fatto che si sta utilizzando primavera, è solo per dare l'idea di base):

@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); 
    } 

} 

e attuare il convertitore come segue:

@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); 
     } 
    } 

} 

Nota che è registrata come @ManagedBean invece di un @FacesConverter. Questo trucco ti consente di iniettare un servizio nel convertitore tramite ad es.@EJB. Vedere anche How to inject @EJB, @PersistenceContext, @Inject, @Autowired, etc in @FacesConverter? Quindi è necessario fare riferimento come converter="#{baseEntityConverter}" anziché converter="baseEntityConverter".

Se vi capita di utilizzare tale convertitore più spesso per i UISelectOne/UISelectMany componenti (<h:selectOneMenu> e amici), si possono trovare OmniFacesSelectItemsConverter molto più utile. Converte in base ai valori disponibili in <f:selectItems> invece di effettuare chiamate DB (potenzialmente costose) ogni volta.

+1

utilizzando la proprietà "id" è non consigliato nei metodi equals e hashCode: si consiglia l'implementazione di equals() e hashCode() utilizzando l'uguaglianza della chiave Business: https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes -equalshashcode.html E per quanto riguarda gli ID compositi? –

0

Ecco la mia soluzione con queste considerazioni:

  • I asume sei interessato a JPA (non Hibernate)
  • La mia soluzione non non richiedono di estende qualsiasi classe e dovrebbe funzionare per qualsiasi entità JPA bean, è solo una semplice classe che usi, né richiede l'implementazione di alcun servizio o DAO. L'unico requisito è che il convertitore dipenda direttamente dalla libreria JPA che potrebbe non essere molto elegante.
  • Utilizza metodi ausiliari per serializzare/deserializzare l'id del bean. Converte solo l'id del bean di entità e compone la stringa con classname e l'id serializzato e convertito in base64. Ciò è possibile a causa del fatto che in jpa gli ID delle entità devono essere serializzabili nell'implementazione di. L'implementazione di questo metodo è in Java 1.7, ma si potrebbe trovare un altro implementazioni per java < 1.7 laggiù
 
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(); 
     } 
    } 

} 

Usalo come un altro convertitore con

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