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.

Thursday, November 1, 2012

Announcement: PrimeFaces Cookbook will be available soon

I'm glad to tell the PrimeFaces Community that the Packt Publisher published an official announcement to the first PrimeFaces book - PrimeFaces Cookbook. The book will give quick solutions to common and advanced use cases. A current table of contents is available on GitHub. As you can see the book size is ca. 410 pages and the release date is February 2013. The book is going to run through the review process now. It will be possible soon to order the book in advance on Amazon, Safari Books Online and other stores.

I would like to thanks all people who accompanied me and my co-writer Mert during the entire writing process with support and suggestions. Thanks!