10 Replies Latest reply on Nov 16, 2011 5:05 AM by hantsy

    Seam 3 entityConverter

    gonzalad

      Hello,


      Is there an entityConverter for Seam 3 ?


      I've found the following information :





      • a draft I think here (didn't tested it though)





      Anyway, in the meantime, I've used the following converter (I'm using it with Seam managed persistence context).


      I's a really rough version.





      package com.natixis.sigma.jsf;
      
      import javax.enterprise.context.RequestScoped;
      import javax.faces.component.UIComponent;
      import javax.faces.context.FacesContext;
      import javax.faces.convert.Converter;
      import javax.faces.convert.FacesConverter;
      import javax.inject.Inject;
      
      @RequestScoped
      @FacesConverter("entityConverter")
      public class EntityConverter  implements Converter {
           @Inject
           private EntityIdentifierStore entityIdentifierStore;
           
           public Object getAsObject(final FacesContext facesContext, final UIComponent component,
                     final String id) {
                return entityIdentifierStore.get(id);
           }
      
           public String getAsString(final FacesContext context, final UIComponent comp, final Object entity) {
                return entityIdentifierStore.put(entity);
           }
      }
      





      public class EntityIdentifierStore implements Serializable {
      
           private List<Identifier> store;
           @Inject
           private EntityManager entityManager;
      
           @PostConstruct
           public void create() {
                store = new ArrayList<Identifier>();
           }
      
           public Object get(String key) {
                try {
                     Identifier identifier = store.get(new Integer(key));
                     return identifier.find(entityManager);
                } catch (IndexOutOfBoundsException e) {
                     return null;
                }
           }
      
           public String put(Object entity) {
                Identifier identifier = createIdentifier(entity);
                if (!store.contains(identifier)) {
                     store.add(identifier);
                }
                return ((Integer) store.indexOf(identifier)).toString();
           }
      
           private Identifier createIdentifier(Object entity) {
                return new Identifier(entity, entityManager);
           }
      }
      





      package com.natixis.sigma.jsf;
      
      import java.io.Serializable;
      
      import javax.persistence.Entity;
      import javax.persistence.EntityManager;
      
      @SuppressWarnings("serial")
      public class Identifier implements Serializable {
      
           public Identifier(Object entity, EntityManager entityManager) {
                this(getEntityClass(entity.getClass()), entityManager
                          .getEntityManagerFactory().getPersistenceUnitUtil()
                          .getIdentifier(entity));
           }
      
           public Identifier(Class clazz, Object id) {
                if (clazz == null || id == null) {
                     throw new IllegalArgumentException("Id and clazz must not be null");
                }
                this.clazz = clazz;
                this.id = id;
           }
      
           private Class clazz;
           private Object id;
      
           public Class getClazz() {
                return clazz;
           }
      
           public Object getId() {
                return id;
           }
      
           @Override
           public boolean equals(Object other) {
                if (other instanceof Identifier) {
                     Identifier that = (Identifier) other;
                     if (id == null || clazz == null) {
                          throw new IllegalArgumentException(
                                    "Class and Id must not be null");
                     } else {
                          return this.getId().equals(that.getId())
                                    && this.getClazz().equals(that.getClazz());
                     }
                }
                return false;
           }
      
           private static Class getEntityClass(Class clazz) {
                while (clazz != null && !Object.class.equals(clazz)) {
                     if (clazz.isAnnotationPresent(Entity.class)) {
                          return clazz;
                     } else {
                          clazz = clazz.getSuperclass();
                     }
                }
                return null;
           }
      
           public Object find(EntityManager entityManager) {
                return entityManager.getReference(getClazz(), getId());
           }
      }
      



      Sample usage (if you're not using Primefaces, just replace p:selectOneMenu with h:selectOneMenu:



      <p:selectOneMenu value="#{portefeuilleBean.criteria.devise}">
           <f:converter converterId="entityConverter"/>
           <f:selectItems value="#{devises}" var="devise"
                itemLabel="#{devise.libelle}" itemValue="#{devise}" />
      </p:selectOneMenu>
      









        • 1. Re: Seam 3 entityConverter
          miguelz

          Thank you for sharing!


          I had to add null value handling to the EntityConverter to be able to define a default value e.g. like this:


          <f:selectItem itemValue="#{null}" itemLabel="..." noSelectionOption="true"/>




          and annotate the EntityIdentifierStore with @ViewScoped



          @ViewScoped
          public class EntityIdentifierStore implements Serializable {
          ...



          Now it seems to work.




          • 2. Re: Seam 3 entityConverter
            gonzalad

            Oups ! Thanks for the feedback

            • 3. Re: Seam 3 entityConverter
              jee4hire
              When I use this solution, entityIdentifierStore == null.
              How can I solve this?


              public class EntityConverter  implements Converter {

                   public String getAsString(final FacesContext context, final UIComponent comp, final Object entity) {
                        return entityIdentifierStore.put(entity);
                   }
              ...
              }


              • 4. Re: Seam 3 entityConverter
                blabno

                Hi, I've run into this exception when using your Identifier:


                java.lang.IllegalArgumentException: Id and clazz must not be null
                     at com.qwestmark.Identifier.<init>(Identifier.java:52)
                     at com.qwestmark.Identifier.<init>(Identifier.java:45)
                     at com.qwestmark.EntityIdentifierStore.createIdentifier(EntityIdentifierStore.java:51)
                     at com.qwestmark.EntityIdentifierStore.put(EntityIdentifierStore.java:42)
                     at com.qwestmark.EntityIdentifierStore$Proxy$_$$_WeldClientProxy.put(EntityIdentifierStore$Proxy$_$$_WeldClientProxy.java)
                     at com.qwestmark.EntityConverter.getAsString(EntityConverter.java:31)
                     at com.qwestmark.EntityConverter$Proxy$_$$_WeldClientProxy.getAsString(EntityConverter$Proxy$_$$_WeldClientProxy.java)
                     at com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.getFormattedValue(HtmlBasicRenderer.java:519)



                The problem was that one of entities was a proxy (due to lazy loading somewhere else), thus persistenceUnit was unable to get identifier from proxy:


                To overcome this i've found  this solution.


                So i've modified the Identifier's constructor like this:


                private static Object unproxy(Object entity)
                    {
                        Hibernate.initialize(entity);
                        if (entity instanceof HibernateProxy) {
                            entity = ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
                        }
                        return entity;
                    }
                
                // --------------------------- CONSTRUCTORS ---------------------------
                
                    public Identifier(Object entity, EntityManager entityManager)
                    {
                        this(getEntityClass(entity.getClass()), entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(unproxy(entity)));
                    }
                



                Bad thing is that now we have hard dependency on hibernate. Anybody has idea on solving this some other way?

                • 5. Re: Seam 3 entityConverter
                  hantsy

                  I noticed there is a ObjectConverter in the Seam Faces source repository now...


                  But from the apidoc, it can be only use in LONG RUN Conversation.

                  • 6. Re: Seam 3 entityConverter
                    hantsy

                    I pasted a version I used in my project...


                    I have done small modification from My Link


                    @FacesConverter("entityConverter")
                    @RequestScoped
                    public class EntityConverter implements javax.faces.convert.Converter,
                              PartialStateHolder {
                    
                         private static final Logger log = LoggerFactory
                                   .getLogger(EntityConverter.class);
                    
                         @Inject
                         private EntityManager entityManager;
                    
                         private Map<String, Serializable> stateMap = new ConcurrentHashMap<String, Serializable>();
                         private boolean initialStateMarked;
                         private boolean isTransient;
                    
                         @Override
                         public Object getAsObject(FacesContext context, UIComponent component,
                                   String value) {
                              if (value == null || "".equals(value)) {
                                   return null;
                              }
                              final ValueExpression ve = component.getValueExpression("value");
                    
                              final Class<?> entityType = ve.getType(context.getELContext());
                    
                              if (log.isDebugEnabled()) {
                                   log.debug("value @" + value);
                              }
                    
                              return this.entityManager.find(entityType, this.stateMap.get(value));
                         }
                    
                         @Override
                         public String getAsString(FacesContext context, UIComponent component,
                                   Object value) {
                              if (value == null) {
                                   return "";
                              }
                              final Serializable id = this.findId(value);
                              final String idKey = value.getClass().getSimpleName() + ":"
                                        + id.hashCode();
                              if (!this.stateMap.containsKey(idKey)) {
                                   this.stateMap.put(idKey, id);
                              }
                    
                              return idKey;
                         }
                    
                         private Serializable findId(Object entity) {
                    
                              final EntityType<?> entityType = this.entityManager.getMetamodel()
                                        .entity(entity.getClass());
                    
                              final Member idAttribute = entityType.getId(
                                        entityType.getIdType().getJavaType()).getJavaMember();
                    
                              Property p = Properties.createProperty(idAttribute);
                    
                              return (Serializable) p.getValue(entity);
                              // return (Serializable) entityManager.getEntityManagerFactory()
                              // .getPersistenceUnitUtil().getIdentifier(entity);
                         }
                    
                         // ---------- State saving --------------
                         @Override
                         public void markInitialState() {
                              this.initialStateMarked = true;
                         }
                    
                         @Override
                         public boolean initialStateMarked() {
                              return this.initialStateMarked;
                         }
                    
                         @Override
                         public void clearInitialState() {
                              this.stateMap = new ConcurrentHashMap<String, Serializable>();
                              this.initialStateMarked = false;
                         }
                    
                         @Override
                         public Object saveState(FacesContext context) {
                              if (context == null) {
                                   throw new IllegalArgumentException("FacesContext must not be null");
                              }
                    
                              return Collections.unmodifiableMap(this.stateMap);
                         }
                    
                         @Override
                         public void restoreState(FacesContext context, Object state) {
                              if (state != null) {
                                   this.stateMap = new ConcurrentHashMap<String, Serializable>(
                                             (Map<? extends String, ? extends Serializable>) state);
                                   return;
                              }
                    
                              this.stateMap.clear();
                         }
                    
                         @Override
                         public boolean isTransient() {
                              return this.isTransient;
                         }
                    
                         @Override
                         public void setTransient(boolean newTransientValue) {
                              this.isTransient = newTransientValue;
                         }
                    }
                    
                    




                    Use in the facelet page..



                    <h:selectOneMenu value="#{crewView.currentCrew.manningAgent}">
                                                       <f:selectItem itemLabel="--Select Manning Travel Agencies--"
                                                            itemValue="#{null}" />
                                                       <f:selectItems value="#{manningAgents}" var="v"
                                                            itemLabel="#{v.fullname}" />
                                                       <f:converter converterId="entityConverter" />
                                                  </h:selectOneMenu>
                    


                    • 7. Re: Seam 3 entityConverter
                      blabno

                      hantsy, thanks for quick response


                      Your code will also have problem when working with proxy.


                      In this line the metamodel won't be able to find entityType from proxy:


                      final EntityType<?> entityType = this.entityManager.getMetamodel().entity(entity.getClass());



                      Even if i do following:


                      final EntityType<?> entityType = this.entityManager.getMetamodel().entity(getEntityClass(entity));
                      
                      private Class getEntityClass(final Object entity) {
                              Class o = entity.getClass();
                              while(!Object.class.equals(o) && o.getAnnotation(Entity.class)==null) {
                                  o = o.getSuperclass();
                              }
                              return Object.class.equals(o) ? entity.getClass() : o;
                          }



                      then findId will have problem:


                      Property p = Properties.createProperty(idAttribute);
                      
                                return (Serializable) p.getValue(entity);



                      cause attribute will be initialized only if we call getter.

                      • 8. Re: Seam 3 entityConverter
                        blabno

                        Ok, now this is pain. If somehow a proxy gets into f:selectItem or f:selectItems then we're doomed, cause upon form submitting javax.faces.component.SelectUtils will try to compare new value with available options (where on can be proxy) and call equals on them.
                        And if your equals operates on fields instead of getters then field in proxy will be uninitialized.
                        This is critical issue IMHO.

                        • 9. Re: Seam 3 entityConverter
                          blabno

                          This error may not be that easy to reproduce.
                          Before we try to render component with f:selectItems or f:selectItem we need to load proxy to EntityManager. Consider this classes:


                          public class Company {
                          
                              @Id private Long id;
                              @ManyToOne(fetch = FetchType.LAZY)  private Language language;
                              //getters/setters/equals/hashCode
                          }




                          public class Language {
                          
                              @Id private Long id;
                              //getters/setters/equals/hashCode
                          }



                          If we have following objects Language(1),Language(2),Language(3),Company(1,Language(2)) (meanning company with id 1 has reference to language with id 2). Now in s:viewAction we load Company(1), due to lazy fetch type the EntityManager will have reference to Company(1) and Language$lazyProxy(2).


                          If our f:SelectItems has value bound to this method:


                          public List<Language> getLanguages() {
                              return entityManager.createQuer("select l from Language l").getResultList();
                          }



                          Then f:selectItems will hold following objects: Language(1),Language$lazyProxy(2),Language(3),Company(1).


                          So now our entityConverters have to be aware of proxies and Language's equals and hashCode must operate on getters instead of fields which are uninitialized for proxies.

                          • 10. Re: Seam 3 entityConverter
                            hantsy

                            Oh...I used a producer for the dropdown box List(application scope)...


                            Did u tried the ObjectConverter in the SNAPSHOT...?