This post is an answer for
BalusC to
our discussion in OmniFaces' issue tracker. The goal is to show how to validate values in an editable
DataTable if they are unique within a column or not. I will not write too much explanation. I will bomb readers with the code :-). The code is written for PrimeFaces
DataTable, but it is easy to adjust it to the JSF standard
DataTable or any other data iteration components like
DataList in PrimeFaces. Validation of unique input values supports lazy loaded tables and dynamic columns as well. First, we will write a tag handler
ValidateUniqueColumn (how to register it in a tag lib. is not a subject of this post).
ValidateUniqueColumn has to be attached to the entire
p:dataTable and have two attributes: int
index (index of the column to be validated) and boolean flag
skipEmpty (if empty values should be skipped when validating; default is
true). XHTML snippet as using example:
<p:dataTable ...>
<p:columns ...>
...
</p:columns>
...
<p:column>
...
</p:column>
<xyz:validateUniqueColumn index="4" skipEmpty="false"/>
<xyz:validateUniqueColumn index="#{bean.columnIndexToValidate}"/>
</p:dataTable>
Implementation of the tag handler:
public class ValidateUniqueColumn extends TagHandler {
public static final String UNSUBSCRIBE_PRERENDER_LISTENERS = "unsubscribePreRenderListeners";
private final TagAttribute index;
private final TagAttribute skipEmpty;
public ValidateUniqueColumn(TagConfig config) {
super(config);
this.index = getRequiredAttribute("index");
this.skipEmpty = getAttribute("skipEmpty");
}
@Override
public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
if (!ComponentHandler.isNew(parent)) {
return;
}
Object objIndex;
if (index.isLiteral()) {
// literal
objIndex = index.getValue();
} else {
// value expression
objIndex = index.getValueExpression(ctx, int.class);
}
Object objSkipEmpty;
if (skipEmpty == null) {
objSkipEmpty = true;
} else if (skipEmpty.isLiteral()) {
// literal
objSkipEmpty = skipEmpty.getValue();
} else {
// value expression
objSkipEmpty = skipEmpty.getValueExpression(ctx, boolean.class);
}
// register a PreRender listener
parent.subscribeToEvent(PreRenderComponentEvent.class, new PreRenderTableListener(objIndex, objSkipEmpty));
// set a flag that all before registered PreRenderTableListener instances must be unsubscribed
parent.getAttributes().put(UNSUBSCRIBE_PRERENDER_LISTENERS, true);
}
}
Implementation of
PreRenderTableListener:
public class PreRenderTableListener implements ComponentSystemEventListener, Serializable {
private static final long serialVersionUID = 20111114L;
private Logger LOG = Logger.getLogger(PreRenderTableListener.class);
private Object index;
private Object skipEmpty;
/**
* This constructor is required for serialization.
*/
public PreRenderTableListener() {
}
public PreRenderTableListener(Object index, Object skipEmpty) {
this.index = index;
this.skipEmpty = skipEmpty;
}
@Override
public void processEvent(ComponentSystemEvent event) {
UIComponent source = event.getComponent();
if (!source.isRendered()) {
return;
}
DataTable dataTable;
if (source instanceof DataTable) {
dataTable = (DataTable) source;
} else {
LOG.warn("Validator ValidateUniqueColumn can be only applied to PrimeFaces DataTable");
return;
}
if (index == null) {
LOG.warn("Column index of the Validator ValidateUniqueColumn is null");
return;
}
Boolean deleteListeners = (Boolean) source.getAttributes().get(
ValidateUniqueColumn.UNSUBSCRIBE_PRERENDER_LISTENERS);
if ((deleteListeners != null) && deleteListeners) {
// unsubscribe all listeners only once - important for AJAX updates
source.getAttributes().remove(ValidateUniqueColumn.UNSUBSCRIBE_PRERENDER_LISTENERS);
Iterator<PostValidateTableListener> iter = getPostValidateTableListeners(dataTable).iterator();
while (iter.hasNext()) {
dataTable.unsubscribeFromEvent(PostValidateEvent.class, iter.next());
}
}
int columnIndex;
if (index instanceof ValueExpression) {
// value expression
Object obj = ((ValueExpression) index).getValue(FacesContext.getCurrentInstance().getELContext());
columnIndex = Integer.valueOf(obj.toString());
} else {
// literal
columnIndex = Integer.valueOf(index.toString());
}
boolean skipEmptyValue;
if (skipEmpty instanceof ValueExpression) {
// value expression
Object obj = ((ValueExpression) skipEmpty).getValue(FacesContext.getCurrentInstance().getELContext());
skipEmptyValue = Boolean.valueOf(obj.toString());
} else {
// literal
skipEmptyValue = Boolean.valueOf(skipEmpty.toString());
}
PostValidateTableListener pvtListener = new PostValidateTableListener(columnIndex, skipEmptyValue);
dataTable.subscribeToEvent(PostValidateEvent.class, pvtListener);
}
protected List<PostValidateTableListener> getPostValidateTableListeners(UIComponent component) {
List<PostValidateTableListener> postValidateTableListeners = new ArrayList<PostValidateTableListener>();
List<SystemEventListener> systemEventListeners = component.getListenersForEventClass(PostValidateEvent.class);
if ((systemEventListeners != null) && !systemEventListeners.isEmpty()) {
for (SystemEventListener systemEventListener : systemEventListeners) {
if (systemEventListener instanceof PostValidateTableListener) {
postValidateTableListeners.add((PostValidateTableListener) systemEventListener);
}
FacesListener wrapped = null;
if (systemEventListener instanceof FacesWrapper<?>) {
wrapped = (FacesListener) ((FacesWrapper<?>) systemEventListener).getWrapped();
}
while (wrapped != null) {
if (wrapped instanceof PostValidateTableListener) {
postValidateTableListeners.add((PostValidateTableListener) wrapped);
}
if (wrapped instanceof FacesWrapper<?>) {
wrapped = (FacesListener) ((FacesWrapper<?>) wrapped).getWrapped();
} else {
wrapped = null;
}
}
}
}
return postValidateTableListeners;
}
}
Implementation of
PostValidateTableListener:
public class PostValidateTableListener implements ComponentSystemEventListener, Serializable {
private static final long serialVersionUID = 20111114L;
private static final Set<VisitHint> VISIT_HINTS = EnumSet.of(VisitHint.SKIP_UNRENDERED);
private int index = -1;
private boolean skipEmpty;
/**
* This constructor is required for serialization.
*/
public PostValidateTableListener() {
}
public PostValidateTableListener(int index, boolean skipEmpty) {
this.index = index;
this.skipEmpty = skipEmpty;
}
public int getIndex() {
return index;
}
@Override
public void processEvent(ComponentSystemEvent event) {
UIComponent source = event.getComponent();
if (!source.isRendered() || (index == -1)) {
return;
}
FacesContext fc = FacesContext.getCurrentInstance();
Map<String, String> requestParamMap = fc.getExternalContext().getRequestParameterMap();
// buffer unique input values during iteration in a list
List<Object> columnValues = new ArrayList<Object>();
DataTable dataTable = (DataTable) source;
int first = dataTable.getFirst();
int rowCount = dataTable.getRowCount();
int rows = dataTable.getRows();
if (dataTable.isLazy()) {
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
if ((rowIndex % rows) == 0) {
dataTable.setFirst(rowIndex);
dataTable.loadLazyData();
}
// get next value of the first editable component in the specified column
Object value = getColumnValue(fc, requestParamMap, dataTable, rowIndex);
// compare with last stored unique values
if (isUnique(fc, columnValues, value)) {
columnValues.add(value);
} else {
break;
}
}
//restore
dataTable.setFirst(first);
dataTable.loadLazyData();
} else {
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
// get next value of the first editable component in the specified column
Object value = getColumnValue(fc, requestParamMap, dataTable, rowIndex);
// compare with last stored unique values
if (isUnique(fc, columnValues, value)) {
columnValues.add(value);
} else {
break;
}
}
//restore
dataTable.setFirst(first);
}
}
private Object getColumnValue(FacesContext fc, Map<String, String> requestParamMap,
DataTable dataTable, int rowIndex) {
dataTable.setRowIndex(rowIndex);
if (!dataTable.isRowAvailable()) {
return null;
}
List<UIColumn> columns = dataTable.getColumns();
if (index < columns.size()) {
int i = -1;
UIColumn foundColumn = null;
for (UIColumn col : columns) {
if (col.isRendered()) {
i++;
}
if (index == i) {
foundColumn = col;
break;
}
}
if (foundColumn == null) {
// column for given index was not found
return null;
}
if (foundColumn instanceof DynamicColumn) {
((DynamicColumn) foundColumn).applyModel();
}
List<UIComponent> children = foundColumn.getChildren();
for (UIComponent component : children) {
// find the first editable rendered component
FirstInputVisitCallback visitCallback = new FirstInputVisitCallback();
component.visitTree(VisitContext.createVisitContext(fc, null, VISIT_HINTS), visitCallback);
EditableValueHolder editableValueHolder = visitCallback.getEditableValueHolder();
if (editableValueHolder != null) {
String clientId = ((UIComponent) editableValueHolder).getClientId(fc);
String value = requestParamMap.get(clientId);
// return converted value for comparison
return ComponentUtils.getConvertedValue(fc, editableValueHolder, value);
}
}
}
return null;
}
private boolean isUnique(FacesContext fc, List<Object> columnValues, Object value) {
if (skipEmpty && ((value == null) || (value.toString().length() < 1))) {
return true;
}
for (Object columnValue : columnValues) {
// compare values with EqualsBuilder from Apache commons project
if (new EqualsBuilder().append(columnValue, value).isEquals()) {
// not unique
fc.addMessage(null, MessageUtils.getMessage("msg_tableNotUniqueValues", index + 1));
fc.validationFailed();
fc.renderResponse();
return false;
}
}
return true;
}
}
How to get converted value from the submitted one is not shown here.
Great post , learned a few things, I have 2 questions
ReplyDelete-why did you need to subscribe a "PreRenderComponentEvent" , couldn't you have skipped that step
directly and subscribe to "PostValidateEvent" from your taghandler, it seems like "PreRenderTableListener "
is acting mainly like a passthrough
-on an unrelated note , i was playing with "timeline" in primefaces extension, when using firefox
if you zoom out with the scroll wheel to the max then try to zoom in with the scroll wheel, it freezes
the browser and you get the "unresponsive script" dialog, same thing happens when i try the js library directly at "http://almende.github.com/chap-links-library/js/timeline/doc/" , it may be better to chose another implementation or find ways to restrict the zooming . What is your opinion of it ?
Thank you , MK .
To PreRenderComponentEvent: this is needed because you can evaluate there values from the ValueExpression for index and skipEmpty attributes. PostValidate phase is too early for that. Also in TagHandler you can not evaluate real values - it the one of the first JSF phase.
ReplyDeleteTo Timeline: Please ask this question in our forum. I'm not the developer of this component.
got it .Thanks for the answer .
ReplyDeleteMK
is this code used for an editable where i am adding rows to this data and not only having static rows displayed in my
ReplyDeleteNotice that when adding a row to a i am updating the table and if any failure validation in previous rows would make no sense after adding which means that validation is skipped and is updated and validated rows will render to with null values