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!

Saturday, October 13, 2012

PrimeFaces Extensions 0.6.1 released

We are glad to announce a new maintenance release of the PrimeFaces Extensions 0.6.1. The main focus was on fixing critical issues. The next release will be also a maintenance release with some improvements / new features for existing components. Our resource optimizer plugin will be upgraded as well. After that you can expect more from this project because I will finish the writing of the "PrimeFaces Cookbook" and we plan to add new components again.

Some quick links to dive into PrimeFaces Extensions:
Getting Started
Showcase Mojarra
Showcase MyFaces

Have fun!

Sunday, October 7, 2012

"Copy to Clipboard" feature in web

Sometimes we need a "Copy to Clipboard" feature to copy some content from an input field or textarea into the OS clipboard. How to implement this feature in JavaScript? There are a lot of articles with this topic in the web. They say - there is only one way to do this cross-browser. This way is Flash. You can read this article or this one and see that a special Flash object is required. A general problem is that installing Flash in browser breaks the paradigm of plain web applications. There is no guarantee that a next Flash update will not break the "Copy to Clipboard" functionality. Also be aware of security restriction by Adobe Flash Player - it doesn’t work at local computer at all.

What is an alternative way? After reading some posts I came to the thought to select the text to be copied automatically and show a tooltip with a little help. The next example demonstrates this idea in JSF with PrimeFaces components. Assume, you have a button "Copy to Clipboard" and an input field with a link (URL) to be copied. Click on the button should call an JavaScript function, say copyInputToClipboard(). As tooltip I will use pe:tooltip from the PrimeFaces Extensions project. The tooltip has showEvent="false" and doesn't get shown by any event. We will show it programmatically by calling widget's method show().
...

<p:commandButton icon="ui-icon ui-icon-copy" type="button"
 value="Copy to Clipboard"
 title="Copy to Clipboard"
 onclick="copyInputToClipboard('#{cc.clientId}:textUrl','widget_#{cc.clientId.replaceAll('-|:','_')}_tip')"/>

...

<p:inputText id="textUrl" value="#{...}" maxlength="2047" autocomplete="off"/>

<pe:tooltip for="textUrl" widgetVar="widget_#{cc.clientId.replaceAll('-|:','_')}_tip"
     value="Press Ctrl+C to copy this link into the clipboard"
     myPosition="top center" atPosition="bottom center" showEvent="false" hideEvent="blur"/>

...
All components are placed within a composite component. This is why I used cc.clientId. The function copyInputToClipboard() expects two parameters: clientId of the input field and the widget variable of the tooltip. In Internet Explorer we can use the object window.clipboardData to achieve the desired functionality. Just call window.clipboardData.setData('text', inputValue) with some input value and the task is done. In other browsers, the best approach would be to select the input text on button click and to show a tooltip with text "Press Ctrl+C to copy this link into the clipboard".
function copyInputToClipboard(inputId, widgetVarTip) {
    if (window.clipboardData && clipboardData.setData) {
        // IE
        var inputValue = $(PrimeFaces.escapeClientId(inputId)).val();
        window.clipboardData.setData('text', inputValue);
    } else {
        // other browsers
        var el = $(PrimeFaces.escapeClientId(inputId));
        el.focus();
        el.select();
        if (widgetVarTip && window[widgetVarTip]) {
            window[widgetVarTip].hide();
            window[widgetVarTip].show();
        }
    }
}
Note that tooltip disappears on blur. The picture demonstrates my implementation.


I hope you have enjoyed this small excursion into the "Copy to Clipboard" feature.

Monday, October 1, 2012

PrimeFaces Extensions 0.6.0 released

We are proud to announce the new release of the PrimeFaces Extensions 0.6.0. It is built on top of PrimeFaces 3.4.1. Please see releases notes.

Main subjects and key features in this release:

  • A lot of bugxifes and improvements / new features in Tooltip, MasterDetail, CKEditor, CodeMirror, InputNumber, AjaxExceptionHandler, ImportConstants, TriStateCheckbox, TriStateManyCheckbox.
  • New component Waypoint which makes a solid base for modern UI patterns that depend on a user's scroll position on a page.
  • New component Switch, regular component to counterparts c:choose, c:when, c:otherwise.
  • BlockPanel was renamed to Spotlight. Spotlight allows you to restrict input to a particular element by masking all other page content.
  • AjaxStatus and Paginator components were removed. PrimeFaces core has the same functionality now.
  • More utilities, like a new EL function to escape jQuery selectors in facelets.
  • Layout component was reimplemented. Layout options are created by model now. You can combine several options in Java like you would build a Tree by Tree nodes. By this way, all possible options are supported. This is only a first step, we didn't add some features like updatable nested (child) layouts. Also state management only works for direct layout panes at the moment due to some issues in current native jQuery Layout plugin (will be fixed soon).
  • Timeline component was reimplemented and improved. This is the fastest Timeline implementation in the world :-) with native JavaScript code in its core parts (without jQuery)! There are a lot of features and about 25 use cases. We have implemented 5. Ask us if you have any questions. Here a screenshot yet (click to enlarge).


This release is available in the Maven central repo as usually.