26

I'm writing my first Java EE 6 web app as a learning exercise. I'm not using a framework, just JPA 2.0, EJB 3.1 and JSF 2.0.

I have a Custom Converter to convert a JPA Entity stored in a SelectOne component back to an Entity. I'm using an InitialContext.lookup to obtain a reference to a Session Bean to find the relevant Entity.

I'd like to create a generic Entity Converter so I don't have to create a converter per Entity. I thought I'd create an Abstract Entity and have all Entities extend it. Then create a Custom Converter for the Abstract Entity and use it as the converter for all Entities.

Does that sound sensible and/or practicable?

Would it make more sense not to have an abstract entity, just a converter that converts any entity? In that case I'm not sure how I'd obtain a reference to the appropriate Session Bean.

I've included my current converter because I'm not sure I'm obtaining a reference to my Session Bean in the most efficient manner.

package com.mycom.rentalstore.converters;

import com.mycom.rentalstore.ejbs.ClassificationEJB;
import com.mycom.rentalstore.entities.Classification;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.convert.FacesConverter;
import javax.naming.InitialContext;
import javax.naming.NamingException;

@FacesConverter(forClass = Classification.class)
public class ClassificationConverter implements Converter {

    private InitialContext ic;
    private ClassificationEJB classificationEJB;

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

        try {
            ic = new InitialContext();
            classificationEJB = (ClassificationEJB) ic.lookup("java:global/com.mycom.rentalstore_RentalStore_war_1.0-SNAPSHOT/ClassificationEJB");

        } catch (NamingException e) {
            throw new ConverterException(new FacesMessage(String.format("Cannot obtain InitialContext - %s", e)), e);
        }

        try {
            return classificationEJB.getClassificationById(Long.valueOf(value));
        } catch (Exception e) {
            throw new ConverterException(new FacesMessage(String.format("Cannot convert %s to Classification - %s", value, e)), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return String.valueOf(((Classification) value).getId());
    }
}
BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
retrodev
  • 2,143
  • 5
  • 22
  • 43
  • it is my allinone class solution: http://stackoverflow.com/a/40069411/4158634 – Jesus M C Feb 24 '17 at 07:07
  • 1
    @BalusC Why is this question considered a "duplicate" as it predates (2010 < 2013) the one which is referenced as the "original". It should have been the other way around. – Xavier Dury Apr 26 '18 at 13:13
  • How is the date important? Moreover, it's closed in 2015. – BalusC Apr 26 '18 at 18:19
  • 1
    @BalusC SO says *This question has been asked **before** and already has an answer* which is not true. Don't take offense, I know you're *THE* super kind/helpful JSF expert on SO and I love omnifaces and everything you do to make JSF better (``) but one could think that, of the 2 questions, you chose to mark as duplicate the (older) one which doesn't have an accepted answer from you in order to promote the one which has your answer as accepted, this could be seen as not playing fair by the people who answered the original question (although I don't think you did that on purpose). – Xavier Dury Apr 27 '18 at 10:17
  • 1
    Indeed. I'm the OP and this is the original question. It should NOT be marked as the duplicate. It was asked first and answered first. – retrodev Apr 27 '18 at 18:49

7 Answers7

8

I am using JSF 2.0 view map:

@FacesConverter("entityConverter")
public class EntityConverter implements Converter {

private static final String key = "com.example.jsf.EntityConverter";
private static final String empty = "";

private Map<String, Object> getViewMap(FacesContext context) {
    Map<String, Object> viewMap = context.getViewRoot().getViewMap();
    @SuppressWarnings({ "unchecked", "rawtypes" })
    Map<String, Object> idMap = (Map) viewMap.get(key);
    if (idMap == null) {
        idMap = new HashMap<String, Object>();
        viewMap.put(key, idMap);
    }
    return idMap;
}

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

@Override
public String getAsString(FacesContext context, UIComponent c, Object value) {
    if (value == null) {
        return empty;
    }
    String id = ((Persistent) value).getId().toString();
    getViewMap(context).put(id, value);
    return id;
}
}
Ecmel Ercan
  • 81
  • 1
  • 1
  • Could you elaborate on the function and purpose of the view map? – retrodev Apr 27 '11 at 09:05
  • It will be eligible for garbage collection after the view is destroyed so there is no need to use a weak hashmap. (It is @ViewScoped so every view will get its own Entity hash map) – Ecmel Ercan Apr 27 '11 at 12:54
7

Well I had the same problem today, and I solved it by creating a generic ConversionHelper and using it in the converter. For this purpose I have an EntityService which is a generic SLSB that I use to perform simple CRUD operations for any entity type. Also my entities implement a PersistentEntity interface, which has a getId and setId methods and I keep them with simple primary keys. That's it.

In the end my converter looks like this:


@FacesConverter(value = "userConverter", forClass = User.class)
public class UserConverter implements Converter {

    @Override
    public Object getAsObject(FacesContext ctx, UIComponent component, java.lang.String value) {

        return ConversionHelper.getAsObject(User.class, value);
    }

    @Override
    public String getAsString(FacesContext ctx, UIComponent component, Object value) {

        return ConversionHelper.getAsString(value);
    }
}

And my conversion helper looks like this:


public final class ConversionHelper {

    private ConversionHelper() {
    }

    public static <T> T getAsObject(Class<T> returnType, String value) {

        if (returnType== null) {

            throw new NullPointerException("Trying to getAsObject with a null return type.");
        }

        if (value == null) {

            throw new NullPointerException("Trying to getAsObject with a null value.");
        }

        Long id = null;

        try {

            id = Long.parseLong(value);

        } catch (NumberFormatException e) {

            throw new ConverterException("Trying to getAsObject with a wrong id format.");
        }

        try {

            Context initialContext = new InitialContext();
            EntityService entityService = (EntityService) initialContext.lookup("java:global/myapp/EntityService");

            T result = (T) entityService.find(returnType, id);

            return result;

        } catch (NamingException e) {

            throw new ConverterException("EntityService not found.");
        }
    }

    public static String getAsString(Object value) {

        if (value instanceof PersistentEntity) {

            PersistentEntity result = (PersistentEntity) value;

            return String.valueOf(result.getId());
        }

        return null;
    }
}

Now creating converters for simple JPA entities is a matter of duplicate a converter and change 3 parameters.

This is working well for me, but I don't know if it is the best approach in terms of style and performance. Any tips would be appreciated.

rbento
  • 5,069
  • 1
  • 40
  • 51
  • Mine is also working well for me. I like my AbstractEntity because it allows me to centralize common boilerplate code such as auditing, PK, toString, hashCode etc. I also use an Abstract Session Bean for simple CRUD that I override when needed. Any comments from more experienced coders as to performance between icwnd and my approach would be very much appreciated. – retrodev Jan 28 '11 at 14:50
  • How does JSF know, which value should be passed in the Converter-Method called getAsObject as the String-param called value? – joshi737 Dec 23 '13 at 18:57
2

my solution is the following :

@ManagedBean
@SessionScoped
public class EntityConverterBuilderBean {
    private static Logger logger = LoggerFactory.getLogger(EntityConverterBuilderBean.class);
    @EJB
    private GenericDao dao;

    public GenericConverter createConverter(String entityClass) {
        return new GenericConverter(entityClass, dao);
    }


}

public class GenericConverter implements Converter {

    private Class clazz;
    private GenericDao dao;

    public GenericConverter(String clazz, Generic dao) {
        try {
            this.clazz = Class.forName(clazz);
            this.dao = dao;
        } catch (Exception e) {
            logger.error("cannot get class: " + clazz, e);
            throw new RuntimeException(e);
        }
    }

    public Object getAsObject(javax.faces.context.FacesContext facesContext, javax.faces.component.UIComponent uiComponent, java.lang.String s) {
        Object ret = null;

        if (!"".equals(s)) {
            Long id = new Long(s);
            ret = dao.findById(clazz, id);
        }

        return ret;
    }

    public String getAsString(javax.faces.context.FacesContext facesContext, javax.faces.component.UIComponent uiComponent, java.lang.Object o) {
        if (o != null) {
            return ((SimpleEntity) o).getId() + "";
        } else {
            return "";
        }
    }
}

and in pages:

 <h:selectOneMenu id="x" value="#{controller.x}" 
      converter="#{entityConverterBuilderBean.createConverter('com.test.model.TestEntity')}">
Arturo Volpe
  • 3,157
  • 3
  • 23
  • 39
alexandrubarbat
  • 177
  • 1
  • 4
1

Try this using Seam Faces from Seam 3.

@Named("DocTypeConverter")
public class DocumentTypeConverter implements Converter, Serializable {

    private static final long serialVersionUID = 1L;

    @Inject
    private DocumentTypeSessionEJB proDocTypeSb;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component,
            String value) {
        DocumentType result = null;

        if (value != null && !value.trim().equals("")) {  
            try {  
                result = (DocumentType) proDocTypeSb.findById(DocumentType.class, value);
            } catch(Exception exception) {  
                throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Conversion Error", "Not a valid value"));  
            }  
        }  

        return result;
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component,
            Object value) {
        String result = null;
        if (value != null && value instanceof DocumentType){
            DocumentType docType = (DocumentType) value;
            result = docType.getId();
        }

        return result;
    }

}
ichalos
  • 457
  • 4
  • 9
  • If SEAM is required then the original specs of the question (JPA 2.0, EJB 3.1, JSF 2.0) is not met. And introducing Seam for just this purpose is just ridiculous. – A.H. Sep 23 '12 at 08:18
  • I know and I agree. But it is working and is does not require the whole Seam package. Just the Seam Faces jars. Besides,there is always the classic way of looking up the Session EJB using JNDI. – ichalos Oct 15 '12 at 11:30
1

(UPDATED FOR JSF 2.3)

I am using something like this:

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

In template, use <f:converter binding="#{entityConverter}" />.

Xavier Dury
  • 1,326
  • 1
  • 13
  • 22
  • Thanks for sharing `component.getValueExpression("value").getType(context.getELContext())` very helpful for generic converter. – djmj Sep 03 '14 at 21:34
  • Starting with JSF 2.3, `@Named` can now be replaced with `@FacesConverter(managed = true)` – Xavier Dury Feb 09 '15 at 08:28
1

Use Seam Faces, it provides a Converter class that does what you want.

org.jboss.seam.faces.conversion.Converter

While it's a JBoss project, Seam 3 works fine with Glassfish 3.1 and newer.

http://seamframework.org/Seam3/FacesModule

On 3.1 it does have a couple of additional dependencies; see http://blog.ringerc.id.au/2011/05/using-seam-3-with-glassfish-31.html

Craig Ringer
  • 259,831
  • 56
  • 584
  • 684
0

To complete the response of Craig Ringer, you can use the generic org.jboss.seam.faces.conversion.ObjectConverter of the Seam 3 FacesModule.

You can grab the code here : https://github.com/seam/faces/blob/develop/impl/src/main/java/org/jboss/seam/faces/conversion/ObjectConverter.java

It uses 2 HashMaps (one is used reversly) and stocks its objects in the Conversation.

Anthony O.
  • 16,644
  • 12
  • 92
  • 151