I have a table in MySQL database. Unfortunately, there is a composite primary key which is needed for JAAS authentication/authorization in GlassFish Server.
mysql> desc group_table;
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| user_group_id | varchar(176) | NO | PRI | NULL | |
| group_id | varchar(15) | NO | PRI | NULL | |
+---------------+--------------+------+-----+---------+-------+
2 rows in set (0.05 sec)
The table contains data in the following format.
mysql> select * from group_table;
+-------------------------+------------+
| user_group_id | group_id |
+-------------------------+------------+
| [email protected] | ROLE_ADMIN |
| [email protected] | ROLE_USER |
| [email protected] | ROLE_USER |
| [email protected] | ROLE_USER |
| [email protected] | ROLE_USER |
+-------------------------+------------+
5 rows in set (0.00 sec)
A <p:dataTable>
with rowKey
works fine , when lazy
is set to false
.
<p:dataTable rowKey="#{row.groupTablePK.userGroupId} #{row.groupTablePK.groupId}">
...
</p:dataTable>
GroupTablePK
is an @Embeddable
class (JPA). The details about this class is not needed I presume.
When lazy
is however, enabled on a <p:dataTable>
, the getRowKey()
and the getRowData()
methods need to be implemented.
How can this be done, when there is a composite primary key which requires a combination of columns as a row key - a unique row identifier?
@Named
@ViewScoped
public class UserAuthorityManagedBean extends LazyDataModel<GroupTable> implements Serializable {
private static final long serialVersionUID = 1L;
@Override
public Object getRowKey(GroupTable groupTable) {
return groupTable != null ? groupTable.getGroupTablePK() : null;
}
@Override
public GroupTable getRowData(String rowKey) {
List<GroupTable> list = (List<GroupTable>) getWrappedData();
System.out.println("rowKey : " + rowKey);
}
@Override
public List<GroupTable> load(int first, int pageSize, List<SortMeta> multiSortMeta, Map<String, Object> filters) {
//... setRowCount(rowCount);
//... Return a List<GroupTable> from a business Service.
}
}
The above implementations are left incomplete.
When a row is selected in the <p:dataTable>
with these implementations, the sout
statement inside the getRowData()
method displays the following.
Info: rowKey : entity.GroupTablePK[ [email protected]
Info: rowKey : groupId=ROLE_USER ]
The getRowKey()
method returns an instance of GroupTablePK
but the getRowData()
method only accepts a String type parameter. It is not an object representing the composite primary key (hereby GroupTablePK
) so that it can be type-cast to an appropriate object type (GroupTablePK
) and based on which an instance of GroupTable
may be obtained from the given List<GroupTable>
and get the getRowData()
method to return that instance of GroupTable
.
How to proceed further?
The question is purely based on the immediate previous question :
java.lang.UnsupportedOperationException: getRowData(String rowKey) must be implemented when basic rowKey algorithm is not used
EDIT:
I have hashcode()
and equals()
implementations in addition to toString()
in GroupTablePK
. The toString()
method in GroupTablePK
returns return "entity.GroupTablePK[ userGroupId=" + userGroupId + ", groupId=" + groupId + " ]";
but the getRowData()
method is invoked twice, when a row in a <p:dataTable>
is selected. It returns the string representation of GroupTablePK
in two parts in two subsequent calls. In the first call, it returns entity.GroupTablePK[ userGroupId=aaa
and then in the second call, it returns groupId=ROLE_USER ]
.
It should instead return entity.GroupTablePK[ userGroupId=aaa, groupId=ROLE_USER ]
at once in a single call.
This kind of comparison groupTable.getGroupTablePK().toString().equals(rowKey)
is therefore not possible which I was thinking about prior to this post. Such as,
@Override
public GroupTable getRowData(String rowKey) {
List<GroupTable> list = (List<GroupTable>) getWrappedData();
for (GroupTable groupTable : list) {
if (groupTable.getGroupTablePK().toString().equals(rowKey)) {
return groupTable;
}
}
return null;
}
EDIT 2:
The following is the shortest possible example removing the JPA noise to reproduce the problem.
Attempted alternatively on,
The behaviour remains stationary on all of these versions of PrimeFaces.
The managed bean:
@Named
@ViewScoped
public class CompositeRowKeyManagedBean extends LazyDataModel<GroupTable> implements Serializable {
private List<GroupTable> selectedValues; // Getter & setter.
private static final long serialVersionUID = 1L;
public CompositeRowKeyManagedBean() {}
private List<GroupTable> init() {
List<GroupTable> list = new ArrayList<GroupTable>();
GroupTablePK groupTablePK = new GroupTablePK("aaa", "ROLE_ADMIN");
GroupTable groupTable = new GroupTable(groupTablePK);
list.add(groupTable);
groupTablePK = new GroupTablePK("bbb", "ROLE_USER");
groupTable = new GroupTable(groupTablePK);
list.add(groupTable);
groupTablePK = new GroupTablePK("ccc", "ROLE_USER");
groupTable = new GroupTable(groupTablePK);
list.add(groupTable);
groupTablePK = new GroupTablePK("ddd", "ROLE_USER");
groupTable = new GroupTable(groupTablePK);
list.add(groupTable);
groupTablePK = new GroupTablePK("eee", "ROLE_USER");
groupTable = new GroupTable(groupTablePK);
list.add(groupTable);
return list;
}
@Override
public List<GroupTable> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters) {
List<GroupTable> list = init();
setRowCount(list.size());
return list;
}
@Override
public Object getRowKey(GroupTable groupTable) {
return groupTable != null ? groupTable.getGroupTablePK() : null;
}
@Override
public GroupTable getRowData(String rowKey) {
List<GroupTable> list = (List<GroupTable>) getWrappedData();
System.out.println("rowKey : " + rowKey);
for (GroupTable groupTable : list) {
if (groupTable.getGroupTablePK().toString().equals(rowKey)) {
return groupTable;
}
}
return null;
}
public void onRowEdit(RowEditEvent event) {
GroupTablePK groupTablePK = ((GroupTable) event.getObject()).getGroupTablePK();
System.out.println("grouoId : " + groupTablePK.getGroupId() + " : userGroupId : " + groupTablePK.getUserGroupId());
}
}
The data table :
<p:dataTable var="row"
value="#{compositeRowKeyManagedBean}"
lazy="true"
editable="true"
selection="#{compositeRowKeyManagedBean.selectedValues}"
rows="50">
<p:column selectionMode="multiple"></p:column>
<p:ajax event="rowEdit" listener="#{compositeRowKeyManagedBean.onRowEdit}"/>
<p:column headerText="GroupId">
<h:outputText value="#{row.groupTablePK.userGroupId}"/>
</p:column>
<p:column headerText="UserGroupId">
<p:cellEditor>
<f:facet name="output">
<h:outputText value="#{row.groupTablePK.groupId}"/>
</f:facet>
<f:facet name="input">
<p:inputText value="#{row.groupTablePK.groupId}"/>
</f:facet>
</p:cellEditor>
</p:column>
<p:column headerText="Edit">
<p:rowEditor/>
</p:column>
</p:dataTable>
When a row is attempted to edit, the onRowEdit()
method is invoked. The getRowData()
is invoked twice and produces a split of the row key in two subsequent calls as said earlier.
These are two domain classes GroupTable
and GroupTablePK
.
public class GroupTable implements Serializable {
private static final long serialVersionUID = 1L;
protected GroupTablePK groupTablePK;
public GroupTable() {}
public GroupTable(GroupTablePK groupTablePK) {
this.groupTablePK = groupTablePK;
}
public GroupTable(String userGroupId, String groupId) {
this.groupTablePK = new GroupTablePK(userGroupId, groupId);
}
public GroupTablePK getGroupTablePK() {
return groupTablePK;
}
public void setGroupTablePK(GroupTablePK groupTablePK) {
this.groupTablePK = groupTablePK;
}
@Override
public int hashCode() {
int hash = 0;
hash += (groupTablePK != null ? groupTablePK.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof GroupTable)) {
return false;
}
GroupTable other = (GroupTable) object;
if ((this.groupTablePK == null && other.groupTablePK != null) || (this.groupTablePK != null && !this.groupTablePK.equals(other.groupTablePK))) {
return false;
}
return true;
}
@Override
public String toString() {
return "entity.GroupTable[ groupTablePK=" + groupTablePK + " ]";
}
}
public class GroupTablePK implements Serializable {
private String userGroupId;
private String groupId;
public GroupTablePK() {}
public GroupTablePK(String userGroupId, String groupId) {
this.userGroupId = userGroupId;
this.groupId = groupId;
}
public String getUserGroupId() {
return userGroupId;
}
public void setUserGroupId(String userGroupId) {
this.userGroupId = userGroupId;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
@Override
public int hashCode() {
int hash = 0;
hash += (userGroupId != null ? userGroupId.hashCode() : 0);
hash += (groupId != null ? groupId.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof GroupTablePK)) {
return false;
}
GroupTablePK other = (GroupTablePK) object;
if ((this.userGroupId == null && other.userGroupId != null) || (this.userGroupId != null && !this.userGroupId.equals(other.userGroupId))) {
return false;
}
if ((this.groupId == null && other.groupId != null) || (this.groupId != null && !this.groupId.equals(other.groupId))) {
return false;
}
return true;
}
@Override
public String toString() {
return "entity.GroupTablePK[ userGroupId=" + userGroupId + ", groupId=" + groupId + " ]";
}
}
I ran your MCVE (kudos to that!) and reproduced it. The rowkey appears to be interpreted as a commaseparated string in the client side to cover the case when multiple selection is needed. This will fail if the string representation of a single rowkey contains a comma, as in your case. The rowkey argument you got in getRowData()
is clear evidence of it: they are the results when the original value is split on comma.
So, to solve this problem, you need to make sure that the getRowKey().toString()
doesn't contain a comma anywhere. Better use a different separator character. E.g. an underscore.
@Override
public Object getRowKey(GroupTable groupTable) {
GroupTablePK pk = groupTable != null ? groupTable.getGroupTablePK() : null;
return pk != null ? pk.getUserGroupId() + "_" + pk.getGroupId() : null;
}
From reading your question I am guessing that the getRowKey() method must return something that is uniquely identifiable to a single row. It is understandable that the underlying JPA entity that represents your row has a composite key object which is fine. The problem I think that is for a Java object to use anything as a key in a Map type collection, the key object must overload and define a proper implementation for the equals
and hashCode
methods.
I suspect that Primefaces probably is using a Map of some kind to retrieve values based on a key. The String type is usually a good candidate for unique key of an object because Strings are immutable and have proper implementations of equals and hashCode. They make a good candidate for this so if you must pass a String to getRowData
then you can always provide a method on that object that returns a unique string for that object. This might be for instance a base 64 representation of the hashCode implementation you provide for your row data object.
If String is not a required parameter then simply implement equals and hashCode for composite key object and use that directly as your key.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With