Tuesday, November 27, 2012

Validate unique input values within DataTable column

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.

4 comments:

  1. Great post , learned a few things, I have 2 questions
    -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 .

    ReplyDelete
  2. 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.

    To Timeline: Please ask this question in our forum. I'm not the developer of this component.

    ReplyDelete
  3. got it .Thanks for the answer .
    MK

    ReplyDelete
  4. is this code used for an editable where i am adding rows to this data and not only having static rows displayed in my
    Notice 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

    ReplyDelete

Note: Only a member of this blog may post a comment.