Question

i have a problem with conversion of entitys with a version. I made a simple example to explain my problem because the "real" application is to big and contains many unnecessary things.

Situation: I have a web application with primefaces and openjpa. I have 20 components (autocompletes + selectedmenues) that needs a converter and they use persistence entitys.

Informations: I only want use jsf,primefaces for it! (Nothing special like omnifaces or something else.) The Question is at bottom. This is only test-code. It is NOT complete and there are some strange things. But this explain my problem at best.

Example entity: (Only fields and hashcode + equals)

@Entity
public class Person {

@Id
private Long id;

private String name;

@Version
private Long version;   

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Person other = (Person) obj;
    if (name == null) {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}
}

First solution: My first solution was that i make a own converter for every component.I inject my managed bean there and use the getter from the "value" of the component.

Bean

@ManagedBean(name = "personBean")
@ViewScoped
public class PersonBean implements Serializable {

private List<Person> persons;
/** unnecessary things **/

xhtml:

<p:selectOneMenu >
<f:selectItems value="#{personBean.persons}"/>              
</p:selectOneMenu>

converter:

@ManagedBean
@RequestScoped
public class PersonConverter implements Converter{

@ManagedProperty(value = "personBean")
private PersonBean personBean;

@Override
public Object getAsObject(FacesContext context, UIComponent component,
        String value) {
    //null & empty checks
    Long id = Long.valueOf(value);
    for(Person person : personBean.getPersons()){
        if(person.getId().equals(id)){
            return person;
        }
    }
    throw new ConverterException("some text");
}

@Override
public String getAsString(FacesContext context, UIComponent component,
        Object value) {
    //null & Instanceof checks
    return String.valueOf(((Person)value).getId());
}
}

Summary: This solution works good. But i found that there must be a better solution as an converter for every component.

Second solution: I found here on stackoverflow the Global Entity Converter. One converter for all, i thought that was a good solution. ("p:autocomplete for a global Entity Converter"). I use it and i thought it works fine. BUT after a few tests i found another big problem, the version of the entity.

Problem1 with entity converter:

I have the version field not in my hashcode or equals (i found nothing about it). I only read this (The JPA hashCode() / equals() dilemma) about it. The problem is that the entity will not replaced in the hashmap and in some cases i get an optimistic locking exception because the "old" entity stays in the hashmap.

 if (!entities.containsKey(entity)) {
            String uuid = UUID.randomUUID().toString();
            entities.put(entity, uuid);
            return uuid;
        } else {
            return entities.get(entity);
        }

Solution: I thought that i can resolve this problem by adding an interface to my entitys that checks whether a version exists.

Interface:

public interface EntityVersionCheck {
public boolean hasVersion();
}

Implementation:

@Override
public boolean hasVersion() {
    return true;
}

Converter:

@Override
public String getAsString(FacesContext context, UIComponent component, Object entity) {
    synchronized (entities) {
        if(entity instanceof EntityVersionCheck && ((EntityVersionCheck)entity).hasVersion()){
            entities.remove(entity);
        }

        if (!entities.containsKey(entity)) {
            String uuid = UUID.randomUUID().toString();
            entities.put(entity, uuid);
            return uuid;
        } else {
            return entities.get(entity);
        }
    }
}

This solution works for the optimistic locking exception but brings another problem!

Problem2 with entity converter:

<p:selectOneMenu value="#{organisation.leader}">
<f:selectItems value="#{personBean.persons}"/>              
</p:selectOneMenu>

If a organisation has already a leader. It will be replaced with a new uuid if the leader is in the persons - list, too. The leader will be set to null or convert exception because the uuid does not exists anymore in the hashmap. It means he use the converter for the organisation.leader and add the leader to the hashmap. Than comes the persons- List and add all other persons in a hashmap and override the uuid from the organisation.leader if he exists in persons, too.

Here are two cases now:

  • When i select a other leader, it works normally.

  • If i dont change the "current" selection and submit the organisation.leader tries to find his "old" uuid but the other person from the person list has override it and the uuid does not exists and the organisation.leader is null.

I found another solution for it and this is my final solution BUT i find, that is a very very strange solution and i will do this better but i found nothing about it.

Final Solution I add the "old" uuid to the "new" object.

 @Override
public String getAsString(FacesContext context, UIComponent component,
        Object entity) {
    synchronized (entities) {
        String currentuuid = null;
        if (entity instanceof EntityVersionCheck
                && ((EntityVersionCheck) entity).hasVersion()) {
            currentuuid = entities.get(entity);
            entities.remove(entity);
        }

        if (!entities.containsKey(entity)) {
            if (currentuuid == null) {
                currentuuid = UUID.randomUUID().toString();
            }
            entities.put(entity, currentuuid);
            return currentuuid;
        } else {
            return entities.get(entity);
        }
    }
}

Question: How i make this better and right?

Was it helpful?

Solution

If Solution 1 worked and you just want it more generic:

Holding your instances within scope of a bean you can use a more generic converter by removing the managed-bean lookup from it. Your entities should inherit from a base entity with a identifier property. You can instantiate this converter in your bean where you retrieved the entities.

Or use a guid map or public identifier if id should not be exposed in html source.

@ManagedBean(name = "personBean")
@ViewScoped
public class PersonBean implements Serializable {

    private List<Person> persons;
    private EntityConverter<Person> converter;

    // this.converter = new EntityConverter<>(persons);
}

<p:selectOneMenu converter="#{personBean.converter}">
    <f:selectItems value="#{personBean.persons}"/>              
</p:selectOneMenu>

Abstract Base Entity converter:

/**
 * Abstract Entity Object JSF Converter which by default converts by {@link Entity#getId()}
 */
public abstract class AEntityConverter<T extends Entity> implements Converter
{
    @Override
    public String getAsString(final FacesContext context, final UIComponent component, final Object value)
    {
        if (value instanceof Entity)
        {
            final Entity entity = (Entity) value;
            if (entity.getId() != null)
                return String.valueOf(entity.getId());
        }

        return null;
    }
}

Cached collection:

/**
 * Entity JSF Converter which holds a Collection of Entities
 */
public class EntityConverter<T extends Entity> extends AEntityConverter<T>
{
    /**
     * Collection of Entity Objects
     */
    protected Collection<T> entities;

    /**
     * Creates a new Entity Converter with the given Entity Object's
     * 
     * @param entities Collection of Entity's
     */
    public EntityConverter(final Collection<T> entities)
    {
        this.entities = entities;
    }

    @Override
    public Object getAsObject(final FacesContext context, final UIComponent component, final String value)
    {
        if (value == null || value.trim().equals(""))
            return null;

        try
        {
            final int id = Integer.parseInt(value);

            for (final Entity entity : this.entities)
                if (entity.getId().intValue() == id)
                    return entity;
        }
        catch (final RuntimeException e)
        {
            // do something --> redirect to exception site
        }

        return null;
    }

    @Override
    public void setEntities(final Collection<T> entities)
    {
        this.entities = entities;
    }

    @Override
    public Collection<T> getEntities()
    {
        return this.entities;
    }
}

Remote or database lookup Converter:

/**
 * Entity Object JSF Converter
 */
public class EntityRemoteConverter<T extends Entity> extends AEntityConverter<T>
{
    /**
     * Dao
     */
    protected final Dao<T> dao;

    public EntityRemoteConverter(final EntityDao<T> dao)
    {
        this.dao = dao;
    }

    @Override
    public Object getAsObject(final FacesContext context, final UIComponent component, final String value)
    {
        // check for changed value
        // no need to hit database or remote server if value did not changed!
        if (value == null || value.trim().equals(""))
            return null;

        try
        {
            final int id = Integer.parseInt(value);
            return this.dao.getEntity(id);
        }
        catch (final RuntimeException e)
        {
            // do someting
        }

        return null;
    }
}

I use dao approach whenever I have to convert view-parameters and bean was not constructed yet.

Avoid expensive lookups In dao approach you should check if the value has changed before doing potential expensive lookups, since converters could be called multiple times within different phases.

Have a look at source of: http://showcase.omnifaces.org/converters/ValueChangeConverter

This basic approach is very flexible and can be extended easily for many use cases.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top