zondag 7 oktober 2007

Eclipse workspaces on the mac.

to create a new eclipse app with a preconfigured workspace:
go to /Applications/eclipse
copy the Eclipse icon.
ctrl (right) click on the new one, and chose show package contents. Go to plist, and change the -data value.
(http://lists.apple.com/archives/Java-dev/2005/Jan/msg00475.html)

dinsdag 2 oktober 2007

jsf Solutions for many2many

Introduction many to many

A challenging and interesting usecase for me has been the implementation of a many-to-many relations in a user interface. I havent found too much documentation or examples on the internet, so I thought I just publish my own efforts here. Note that here is quite some documentation on the ejb (jpa) modelling, but the actual gui implementation is missing in most cases.
Please note that this example is part of my "project" accountancy for sportclubs, see earliest blog for more details.

Technology


The versions used in this example: jboss 4.2 (ejb3), seam 1.2.1, mysql 5.0m icefaces 1.6.0 and jsf-ri (?.?)

Use case description & data model



I have a subject (which may be of interest to our club: a person, store selling us volleybals, the room landlord etc) Furter I have roles, some of the roles will be connected to posts on the balance sheets (like credit, trainer, rent or debitor: compition player, recreational player). A subject performing a role becomes an actor. Of course a role maybe played by many actors, and a subject may fulfill many roles.
The actor is a period based entity, during a season, the next season he may give up his role as trainer, or give up his role as competition member.

In ejb3 the possibility exists to directly indicate a many-to-many relation ship using an annotation with the same name and indicating a join-table. However I'm not convinced this is the way to go to map many-to-many relations. You loose control over your join table (in my use case, an actor exists for a certain period, for a new period the actor must be cloned), see also this blog, where the author introduces some life in the join table. However in the end it turns out to be dead meat (see question 6 and 7).

So instead of modeling a many2many relationship I modeled 2 one-to-many relationships. I'm not completely sure this is the right approach but for me it worked well. The 2 one-2-many relationships are intuitively mapped in the entities Subject, Actor and Role. See code below.

Gui


We're approaching our final goal: how to display this in a gui. I have chosen for a master detail view: A list of all subjects, clicking on one subject presents you the subject in focus and the possibility to edit the subject's attributes. I'll leave vanilla attributes out of scope, and only look on how to select the roles the user wishes the subject to fulfill (role creation is left out of this use case). For this list I have tried two different implementations a ice:selectManyListbox and an ice:dataTable toegether with ice:rowSelector. Both having pros and cons which will be explained in (earlier) posts below and summarized in the last section. For now have fun with the generated entity classes.

Thanks to


This blog wouldn't definitely have made it without the jboss seam and iceface guys, Thanks! Also thanks to the contributors of both forums who have helped me out on numerous occasions. Thanks also to my colleagues Bertram and Faizal who have helped me out on the "elegant" query. And finally to Ellen who has withstood all my swearing and cursing on the css and the blogspot templates. And was patient enough to let me it all figure out...
Jeroen.

Subject.java

001 package nl.jeroen.testdb.persist;
002 // Generated Dec 23, 2006 5:43:17 PM by Hibernate Tools 3.2.0.snapshotb9
003
004 import java.util.ArrayList;
024 import ....;
024 import org.jboss.seam.log.Log;
025
026 /**
027 * Subject generated by hbm2java
028 */
029 @Name("subject")
030 @Entity
031 @Scope(ScopeType.SESSION)
032 @Table(name = "subject", catalog = "seam", uniqueConstraints = {})
033 public class Subject implements java.io.Serializable {
034
035 // Fields
036 @org.jboss.seam.annotations.Logger
037 private Log logger;
038 private int subjectid;
039 private String info;
040 private String firstname;
041 private String lastname;
042 private String email;
043 private List<Actor> actors = new ArrayList<Actor>(0);
044
119 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "subject")
120 public List<Actor> getActors() {
121 return this.actors;
122 }
123
124 public void setActors(List<Actor> actors) {
125 this.actors = actors;
126 }
127
128 }


Actor.java

001 package nl.jeroen.testdb.persist;
002
003 import java.util.HashSet;
015 import ..;
016 import org.hibernate.validator.NotNull;
017
018 /**
019 * Actor generated by hbm2java
020 */
021 @Entity
022 @Table(name = "actor", catalog = "seam", uniqueConstraints = {@UniqueConstraint(columnNames = {
023 "roleid", "subjectid", "periodid"})})
024 public class Actor implements java.io.Serializable {
025
026 // Fields
027
028 private int actorid;
029 private Period period;
030 private Role role;
031 private Subject subject;
032 private Set<Transaction> transactionsForDestinationid = new HashSet<Transaction>(
033 0);
034 private Set<Transaction> transactionsForSourceid = new HashSet<Transaction>(
035 0);
036
037 // Constructors
038
039 /** default constructor */
040 public Actor() {
041 }
042
043 /** minimal constructor */
044 public Actor(int actorid, Period period, Role role, Subject subject) {
045 this.actorid = actorid;
046 this.period = period;
047 this.role = role;
048 this.subject = subject;
049 }
050 /** full constructor */
051 public Actor(int actorid, Period period, Role role, Subject subject,
052 Set<Transaction> transactionsForDestinationid,
053 Set<Transaction> transactionsForSourceid) {
054 this.actorid = actorid;
055 this.period = period;
056 this.role = role;
057 this.subject = subject;
058 this.transactionsForDestinationid = transactionsForDestinationid;
059 this.transactionsForSourceid = transactionsForSourceid;
060 }
061
062 // Property accessors
063 @Id @GeneratedValue
064 @Column(name = "actorid", unique = true, nullable = false)
065 @NotNull
066 public int getActorid() {
067 return this.actorid;
068 }
069
070 public void setActorid(int actorid) {
071 this.actorid = actorid;
072 }
073 @ManyToOne(fetch = FetchType.LAZY)
074 @JoinColumn(name = "periodid", nullable = false)
075 @NotNull
076 public Period getPeriod() {
077 return this.period;
078 }
079
080 public void setPeriod(Period period) {
081 this.period = period;
082 }
083 @ManyToOne(fetch = FetchType.LAZY)
084 @JoinColumn(name = "roleid", nullable = false)
085 @NotNull
086 public Role getRole() {
087 return this.role;
088 }
089
090 public void setRole(Role role) {
091 this.role = role;
092 }
093 @ManyToOne(fetch = FetchType.LAZY)
094 @JoinColumn(name = "subjectid", nullable = false)
095 @NotNull
096 public Subject getSubject() {
097 return this.subject;
098 }
099
100 public void setSubject(Subject subject) {
101 this.subject = subject;
102 }
103 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "actorByDestinationid")
104 public Set<Transaction> getTransactionsForDestinationid() {
105 return this.transactionsForDestinationid;
106 }
107
108 public void setTransactionsForDestinationid(
109 Set<Transaction> transactionsForDestinationid) {
110 this.transactionsForDestinationid = transactionsForDestinationid;
111 }
112 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "actorBySourceid")
113 public Set<Transaction> getTransactionsForSourceid() {
114 return this.transactionsForSourceid;
115 }
116
117 public void setTransactionsForSourceid(
118 Set<Transaction> transactionsForSourceid) {
119 this.transactionsForSourceid = transactionsForSourceid;
120 }
121
122 }


Role.java

001 package nl.jeroen.testdb.persist;
002 // Generated Dec 23, 2006 5:43:17 PM by Hibernate Tools 3.2.0.snapshotb9
003
004 import java.util.HashSet;
012 import ..;
018 import org.hibernate.validator.NotNull;
019
020 /**
021 * Role generated by hbm2java
022 */
023 @Entity
024 @Table(name = "role", catalog = "seam", uniqueConstraints = {})
025 public class Role implements java.io.Serializable {
026 /**
027 *
028 */
029 private static final long serialVersionUID = -1996329923962792444L;
030
031 private static final org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory
032 .getLog(Role.class);
033 // Fields
034
035 private int roleid;
036 private String info;
037 private Integer parentid;
038 private String name;
039 private Set<Actor> actors = new HashSet<Actor>(0);
040
041 // Constructors
103 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "role")
104 public Set<Actor> getActors() {
105 return this.actors;
106 }
107
108 public void setActors(Set<Actor> actors) {
109 this.actors = actors;
110 }
111
112 @Override
113 public int hashCode() {
114 return roleid;
115 }
116
117 @Override
118 public boolean equals(Object obj) {
119 final String methodName = "equals";
120 if (logger.isTraceEnabled()) {
121 logger.trace(LogUtil.getLogMessage(LogUtil.VERSION, methodName
122 ,"start '" + "'"));
123 }
124 if (obj == null) {
125 return false;
126 }
127 if (this == obj) {
128 return true;
129 }
130 if (obj instanceof Role
131 && ((Role)obj).getRoleid() == roleid) {
132 return true;
133 }
134 return false;
135 }
136 }

jsf Solutions for many2many: the selectManyListBox

SelectManyListbox.



The selectManyListBox is an ordinary dropdown listbox with a list of selected values opposed to the single value for its SelectOneListbox sibbling. Unfortunately at the moment of implementing the ice:selectManyListbox was still buggy: the menurenderer.convertSelectValue didnot call the associated converter for multiple values. Apparently things have been ironed out in icefaces 1.6.1 Check the iceforum for a more detailed explanation.

<h2>Roles</h2>
<ice:selectManyListbox id="selectedRoles" value="#{subjectMgr.selectedRoles}" converter="#{subjectMgr.roleConverter}" size="5">
  <f:selectItems value="#{subjectMgr.allAvailableRoles}"/>
</ice:selectManyListbox>

You need 2 lists for selectmany listbox: a list with all available roles, individually stored into a selectitem and (2) a list with the selected roles. A converter (I think there is a seam tag, to circumvent the converter but at the moment of experimenting that was still in beta) to convert the values inside the selectitem tag to some unique serializable key (string) for rendering in the select tag and vice versa (in my case ordinary findById methods).
A successful display of the selectmanylistbox is not that hard, the hardest part is storing the selected and deleting the unselected roles.

Check the code below for SubjectMgrBean.setSelectedRoles. The implementation I came up with was a maintenance of three lists: the new list (originating from the selectManyListBox component), the list with actors to remove (A map with a Role - Actor mapping). And the list with actors to be created. The rolesToAdd and the rolesToRemove lists are initialized by the current roles on the subject in focus. For the removal we take the presentRoles (roles to Remove) exclude all roles in the newly selected roles, and we're are on our own with the roles which are up for deletion. An iteration is needed because in jpa-ql it is not possible to do a cascaded delete on a parent entity.
A similar operation is used for the roles to add. All roles appearing in the presentRoles, are removed from the selected roles leaving a collection with roles to add. Every Role is attached to the subject in focus, by creating a new Actor and adding it the set of existing actors. A final merge on the entity manager is sufficient to store all roles (actors) at once.
As you have noticed the code for the selectmanylistbox may not be the world's cleanest code. But I'm convinced that a lot of plumbing is needed to get this to work. The amount of code for such a simple use case is also contra intuitive Also major drawback is the big bug in icefaces forcing you to either hack their code or using the reference-jsf's selectmanylistbox implemention. Particular caveat is the implementation of the equals method of the entity beans. However the most annoying feature is the gui element automatically disselect all choices in the box when the user hits a wrong element without the cmd key pressed (on iMac), leaving nothing but the cancel button to the user. If i would have realized this feature earlier I probably would have never taken the selectmanylistbox for this use case.
PS the last part can be found in September.

SubjectMgrBean.java

001 package nl.jeroen.testdb.persist;
002 
003 import java.io.Serializable;
038 import org.jboss.seam.annotations.datamodel.DataModelSelection;
039 import ....;
041 import com.icesoft.util.DebugException;
042 
043 @Scope(ScopeType.CONVERSATION)
044 @Stateful
045 @Name("subjectMgr")
046 public class SubjectMgrBean implements SubjectMgr, Serializable {
047     @Logger
048     Log logger;
049         
050     public static final String EMPTY = "__EMPTY__";
051     private static final long serialVersionUID = 8523599219865145149L;
052     private Map<String, Role> roleValues;
053     private List<SelectItem> allAvailableRoles;
054     private List<Role> selectedRoles;
055     
056     @In(required=true)
057     IApplicationSession sessionstore;
058     
059     @PersistenceContext(type=PersistenceContextType.EXTENDED)
060     private EntityManager em;
061     
062     @In(required=false)
063     @Out(required=false)
064     private Subject subject;
065 
066     @DataModel(value="subjectList")
067     List<Subject> subjectList;
068     @DataModelSelection(value="subjectList")
069     @Out(required=false, value="focusSubject")
070     private Subject focusSubject;
071 
072     public List<SelectItem> getAllAvailableRoles() {
073         final String methodName = "getAllAvailableRoles: ";
074         if (logger.isTraceEnabled()) {
075             logger.trace(LogUtil.getLogMessage(methodName, LogUtil.VERSION,
076                     "starting '" "'"));
077         }
078         boolean populateRoleValues = false;
079 //        List<SelectItem> result = null;
080         if (allAvailableRoles == null || allAvailableRoles.size() == 0) {
081             List<Role> l = em.createQuery("from Role role").getResultList();
082             allAvailableRoles = new ArrayList<SelectItem>(l.size());
083             if (roleValues == null) {
084                 populateRoleValues = true;
085                 roleValues = new TreeMap<String, Role>();
086             }
087             for (Role r : l) {
088                 SelectItem item = new SelectItem(r,r.getName());
089                 allAvailableRoles.add(item);
090                 if (populateRoleValues) {
091                     roleValues.put(r.getName(),r);
092                 }
093             }
094             
095         }
096         if (logger.isDebugEnabled()) {
097             logger.debug(LogUtil.getLogMessage(methodName, LogUtil.VERSION,
098                 "ending with  '" + allAvailableRoles.size() "' in available roles"));
099         }
100         return allAvailableRoles;
101     }
102     public void setAllAvailableRoles(List<SelectItem> allAvailableRoles) {
103         this.allAvailableRoles = allAvailableRoles;
104     }
105     
106     public List<Role> getSelectedRoles() {
107         final String methodName = "getSelectedRoles: ";
108         if (logger.isTraceEnabled()) {
109             logger.trace(LogUtil.getLogMessage(methodName, LogUtil.VERSION,
110                     "starting '" + focusSubject.getSubjectid() "'"));
111         }
112         selectedRoles = em.createQuery("Select r from Role r, in (r.actors) a where a.subject = :subject")
113             .setParameter("subject", focusSubject).getResultList();
114         if (logger.isDebugEnabled()) {
115             logger.debug(LogUtil.getLogMessage(methodName, LogUtil.VERSION, "returning '" + selectedRoles.size() "'"));
116         }
117         return selectedRoles;
118     }
119     public void setSelectedRoles(List<Role> sltdRoles) {
120         final String methodName = "setSelectedRoles: ";
121         if (logger.isDebugEnabled()) {
122             logger.debug(LogUtil.getLogMessage(methodName,
123                     LogUtil.VERSION, "starting '" + sltdRoles.size() "'"));
124         }
125         try {
126             if (logger.isDebugEnabled()) {
127                 for (Role r : sltdRoles) {
128                     logger.debug(LogUtil.getLogMessage(methodName,
129                             LogUtil.VERSION, "selected roles: '" + r.getName() +  "' (" + r + ")"));
130                 }
131             }
132             Period prd = sessionstore.getFocusPeriod();
133             logger.info("focust period is #0 name is '#1'", prd.getPeriodid(), prd.getInfo());
134             Map<Role,Actor> presentRolesToRemove = new HashMap<Role, Actor>();
135             Set<Role> presentRolesToCreate = new HashSet<Role>();
136             for (Actor act :focusSubject.getActors()) {
137                 presentRolesToRemove.put(act.getRole(), act);
138                 presentRolesToCreate.add(act.getRole());
139             }
140             presentRolesToRemove.keySet().removeAll(sltdRoles);
141             for (Role r : presentRolesToRemove.keySet()) {
142                 Actor act = presentRolesToRemove.get(r);
143                 focusSubject.getActors().remove(act);
144                 em.remove(act);
145             }
146             sltdRoles.removeAll(presentRolesToCreate);
147             for (Role r : sltdRoles) {
148                 focusSubject.getActors().add(new Actor(-1,sessionstore.getFocusPeriod(), r, focusSubject));
149             }
150             em.merge(focusSubject);
151         catch (Throwable e) {
152             logger.error("error ocurrued", e);
153         }
154     }
155     
156     public Converter getRoleConverter() {
157         return new Converter (){
158             
159             public Object getAsObject(FacesContext arg0, UIComponent arg1, String arg2throws ConverterException {
160                 final String methodName = "getAsObject";
161                 if (logger.isTraceEnabled()) {
162                     logger.trace(LogUtil.getLogMessage(LogUtil.VERSION, methodName
163                         "start with argument '" + arg2 + "'"));
164                 }
165                 if (arg2 == null || arg2.equals(""|| arg2.equals(EMPTY)) {
166                     return null;
167                 }
168                 try {
169                     int roleid = Integer.parseInt(arg2);
170                     Iterator<SelectItem> iter = allAvailableRoles.iterator();
171                     boolean found = false;
172                     Role result = null;
173                     while (!found && iter.hasNext()) {
174                         result = (Roleiter.next().getValue();
175                         if (result.getRoleid() == roleid
176                             found = true;
177                     }
178                     try {
179                         EntityManager em1 = (EntityManagerComponent.getInstance("entityManager"true);
180                         em1.find(Role.class, roleid);
181                     catch (Exception e) {
182                         logger.error(LogUtil.getLogMessage(methodName,
183                                 LogUtil.VERSION, "Error occured retrieving them from inner class."), e);
184                     }
185                     if (logger.isDebugEnabled()) {
186                         logger.debug(LogUtil.getLogMessage(methodName,
187                                 LogUtil.VERSION, "'" + roleid + "'"));
188                     }
189                     return result;
190                 catch (NumberFormatException nfe) {
191                     logger.error(LogUtil.getLogMessage(LogUtil.VERSION, methodName 
192                         "passed argument '" + arg2 + "' cannot converted to int"), nfe);
193                 }
194                 return null;
195             }
196     
197             public String getAsString(FacesContext arg0, UIComponent arg1, Object arg2throws ConverterException {
198                 final String methodName = "getAsString";
199                 if (logger.isTraceEnabled()) {
200                     logger.trace(LogUtil.getLogMessage(LogUtil.VERSION, methodName
201                         "start '" + arg2 + "' of type '" (arg2 != null ? arg2.getClass() """'"));
202                 }
203                 if (arg2 == nullreturn EMPTY;
204                 if ("-1".equals(arg2)) return EMPTY;
205                 if (arg2 instanceof Role) {
206                     return "" ((Role)arg2).getRoleid();
207                 else {
208                     logger.error(LogUtil.getLogMessage(LogUtil.VERSION, methodName 
209                         "argument is not expected type Role returning _EMPTY"));
210                 }
211                 return EMPTY;
212             }
213         };
214         
215     }
216 
217 
218     public Role findRoleById(Integer id) {
219         return (Roleem.createQuery("r from Role r where r.roleid = :id").setParameter("id", id).getSingleResult();
220     }
221