View Javadoc
1   /*
2    * Copyright (c) 2011-2024 PrimeFaces Extensions
3    *
4    *  Permission is hereby granted, free of charge, to any person obtaining a copy
5    *  of this software and associated documentation files (the "Software"), to deal
6    *  in the Software without restriction, including without limitation the rights
7    *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    *  copies of the Software, and to permit persons to whom the Software is
9    *  furnished to do so, subject to the following conditions:
10   *
11   *  The above copyright notice and this permission notice shall be included in
12   *  all copies or substantial portions of the Software.
13   *
14   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20   *  THE SOFTWARE.
21   */
22  package org.primefaces.extensions.component.sheet;
23  
24  import java.io.IOException;
25  import java.lang.reflect.Array;
26  import java.util.Collection;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  
31  import javax.faces.FacesException;
32  import javax.faces.component.UIComponent;
33  import javax.faces.component.behavior.ClientBehavior;
34  import javax.faces.component.behavior.ClientBehaviorContext;
35  import javax.faces.component.behavior.ClientBehaviorHolder;
36  import javax.faces.context.FacesContext;
37  import javax.faces.context.ResponseWriter;
38  import javax.faces.model.SelectItem;
39  
40  import org.primefaces.behavior.ajax.AjaxBehavior;
41  import org.primefaces.extensions.util.Attrs;
42  import org.primefaces.extensions.util.ExtLangUtils;
43  import org.primefaces.extensions.util.JavascriptVarBuilder;
44  import org.primefaces.renderkit.CoreRenderer;
45  import org.primefaces.shaded.json.JSONArray;
46  import org.primefaces.shaded.json.JSONException;
47  import org.primefaces.shaded.json.JSONObject;
48  import org.primefaces.util.Constants;
49  import org.primefaces.util.FacetUtils;
50  import org.primefaces.util.HTML;
51  import org.primefaces.util.LangUtils;
52  import org.primefaces.util.WidgetBuilder;
53  
54  /**
55   * The Sheet renderer.
56   *
57   * @author Mark Lassiter / Melloware
58   * @since 6.2
59   */
60  public class SheetRenderer extends CoreRenderer {
61  
62      /**
63       * Encodes the Sheet component
64       */
65      @Override
66      public void encodeEnd(final FacesContext context, final UIComponent component) throws IOException {
67          final ResponseWriter responseWriter = context.getResponseWriter();
68          final Sheet sheet = (Sheet) component;
69  
70          // update column mappings on render
71          sheet.updateColumnMappings();
72  
73          // sort data
74          sheet.sortAndFilter();
75  
76          // encode markup
77          encodeMarkup(context, sheet, responseWriter);
78  
79          // encode javascript
80          encodeScript(context, sheet);
81      }
82  
83      /**
84       * Encodes the HTML markup for the sheet.
85       */
86      protected void encodeMarkup(final FacesContext context, final Sheet sheet, final ResponseWriter responseWriter)
87                  throws IOException {
88  
89          /*
90           * <div id="..." name="..." class="" style="">
91           */
92          final String styleClass = sheet.getStyleClass();
93          final String clientId = sheet.getClientId(context);
94          final Integer width = sheet.getWidth();
95          final Integer height = sheet.getHeight();
96          String style = sheet.getStyle();
97  
98          // outer div to wrapper table
99          responseWriter.startElement("div", null);
100         responseWriter.writeAttribute("id", clientId, "id");
101         responseWriter.writeAttribute("name", clientId, "clientId");
102         // note: can't use ui-datatable here because it will mess with
103         // handsontable cell rendering
104         String divclass = "ui-handsontable ui-widget";
105         if (styleClass != null) {
106             divclass = divclass + " " + styleClass;
107         }
108         if (!sheet.isValid()) {
109             divclass = divclass + " ui-state-error";
110         }
111 
112         responseWriter.writeAttribute(Attrs.CLASS, divclass, "styleClass");
113         if (width != null) {
114             responseWriter.writeAttribute(Attrs.STYLE, "width: " + width + "px;", null);
115         }
116 
117         encodeKeyboardTrap(context, sheet);
118         encodeHiddenInputs(context, sheet, clientId);
119         encodeFilterValues(responseWriter, sheet, clientId);
120         encodeHeader(context, responseWriter, sheet);
121 
122         // handsontable div
123         responseWriter.startElement("div", null);
124         responseWriter.writeAttribute("id", clientId + "_tbl", "id");
125         responseWriter.writeAttribute("name", clientId + "_tbl", "clientId");
126         responseWriter.writeAttribute(Attrs.CLASS, "handsontable-inner", "styleClass");
127 
128         if (style == null) {
129             style = Constants.EMPTY_STRING;
130         }
131 
132         if (width != null) {
133             style = style + "width: " + width + "px;";
134         }
135 
136         if (height != null) {
137             style = style + "height: " + height + "px;";
138         }
139         else {
140             style = style + "height: 100%;";
141         }
142 
143         responseWriter.writeAttribute(Attrs.STYLE, style, null);
144 
145         responseWriter.endElement("div");
146         encodeFooter(context, responseWriter, sheet);
147         responseWriter.endElement("div");
148     }
149 
150     /**
151      * Encodes an optional attribute to the widget builder specified.
152      *
153      * @param wb the WidgetBuilder to append to
154      * @param attrName the attribute name
155      * @param value the value
156      * @throws IOException if any IO error occurs
157      */
158     protected void encodeOptionalAttr(final WidgetBuilder wb, final String attrName, final String value)
159                 throws IOException {
160         if (value != null) {
161             wb.attr(attrName, value);
162         }
163     }
164 
165     /**
166      * Encodes an optional native attribute (unquoted).
167      *
168      * @param wb the WidgetBuilder to append to
169      * @param attrName the attribute name
170      * @param value the value
171      * @throws IOException if any IO error occurs
172      */
173     protected void encodeOptionalNativeAttr(final WidgetBuilder wb, final String attrName, final Object value)
174                 throws IOException {
175         if (value != null) {
176             wb.nativeAttr(attrName, value.toString());
177         }
178     }
179 
180     /**
181      * Encodes the Javascript for the sheet.
182      *
183      * @param context the FacesContext
184      * @param sheet the Sheet
185      * @throws IOException if any IO error occurs
186      */
187     protected void encodeScript(final FacesContext context, final Sheet sheet)
188                 throws IOException {
189         final WidgetBuilder wb = getWidgetBuilder(context);
190         wb.init("ExtSheet", sheet);
191 
192         // errors
193         encodeInvalidData(sheet, wb);
194         // data
195         encodeData(context, sheet, wb);
196 
197         // the delta var that will be used to track changes client side
198         // stringified and placed in hidden input for submission
199         wb.nativeAttr("delta", "{}");
200 
201         // filters
202         encodeFilterVar(sheet, wb);
203         // sortable
204         encodeSortVar(sheet, wb);
205         // behaviors
206         encodeBehaviors(context, sheet, wb);
207 
208         encodeOptionalNativeAttr(wb, "readOnly", sheet.isReadOnly());
209         encodeOptionalNativeAttr(wb, "fixedColumnsLeft", sheet.getFixedCols());
210         encodeOptionalNativeAttr(wb, "fixedRowsTop", sheet.getFixedRows());
211         encodeOptionalNativeAttr(wb, "fixedRowsBottom", sheet.getFixedRowsBottom());
212         encodeOptionalNativeAttr(wb, "manualColumnResize", sheet.isResizableCols());
213         encodeOptionalNativeAttr(wb, "manualRowResize", sheet.isResizableRows());
214         encodeOptionalNativeAttr(wb, "manualColumnMove", sheet.isMovableCols());
215         encodeOptionalNativeAttr(wb, "manualRowMove", sheet.isMovableRows());
216         encodeOptionalNativeAttr(wb, "allowTabOffSheet", sheet.isAllowTabOffSheet());
217         encodeOptionalNativeAttr(wb, "width", sheet.getWidth());
218         encodeOptionalNativeAttr(wb, "height", sheet.getHeight());
219         encodeOptionalNativeAttr(wb, "minRows", sheet.getMinRows());
220         encodeOptionalNativeAttr(wb, "minCols", sheet.getMinCols());
221         encodeOptionalNativeAttr(wb, "maxRows", sheet.getMaxRows());
222         encodeOptionalNativeAttr(wb, "maxCols", sheet.getMaxCols());
223         encodeOptionalAttr(wb, "stretchH", sheet.getStretchH());
224         encodeOptionalAttr(wb, "language", sheet.getLocale());
225         encodeOptionalAttr(wb, "selectionMode", sheet.getSelectionMode());
226         encodeOptionalAttr(wb, "activeHeaderClassName", sheet.getActiveHeaderStyleClass());
227         encodeOptionalAttr(wb, "commentedCellClassName", sheet.getCommentedCellStyleClass());
228         encodeOptionalAttr(wb, "currentRowClassName", sheet.getCurrentRowStyleClass());
229         encodeOptionalAttr(wb, "currentColClassName", sheet.getCurrentColStyleClass());
230         encodeOptionalAttr(wb, "currentHeaderClassName", sheet.getCurrentHeaderStyleClass());
231         encodeOptionalAttr(wb, "invalidCellClassName", sheet.getInvalidCellStyleClass());
232         encodeOptionalAttr(wb, "noWordWrapClassName", sheet.getNoWordWrapStyleClass());
233         encodeOptionalAttr(wb, "placeholderCellClassName", sheet.getPlaceholderCellStyleClass());
234         encodeOptionalAttr(wb, "readOnlyCellClassName", sheet.getReadOnlyCellStyleClass());
235         encodeOptionalNativeAttr(wb, "extender", sheet.getExtender());
236 
237         String emptyMessage = sheet.getEmptyMessage();
238         if (LangUtils.isBlank(emptyMessage)) {
239             emptyMessage = "No Records Found";
240         }
241         encodeOptionalAttr(wb, "emptyMessage", emptyMessage);
242 
243         encodeColHeaders(sheet, wb);
244         encodeColOptions(sheet, wb);
245         wb.finish();
246     }
247 
248     /**
249      * Encodes the necessary JS to render invalid data.
250      *
251      * @throws IOException if any IO error occurs
252      */
253     protected void encodeInvalidData(final Sheet sheet, final WidgetBuilder wb)
254                 throws IOException {
255         wb.attr("errors", sheet.getInvalidDataValue());
256     }
257 
258     /**
259      * Encode the column headers
260      *
261      * @throws IOException if any IO error occurs
262      */
263     protected void encodeColHeaders(final Sheet sheet, final WidgetBuilder wb)
264                 throws IOException {
265         final JavascriptVarBuilder vb = new JavascriptVarBuilder(null, false);
266         for (final SheetColumn column : sheet.getColumns()) {
267             if (!column.isRendered()) {
268                 continue;
269             }
270             vb.appendArrayValue(column.getHeaderText(), true);
271         }
272         wb.nativeAttr("colHeaders", vb.closeVar().toString());
273     }
274 
275     /**
276      * Encode the column options
277      *
278      * @throws IOException if any IO error occurs
279      */
280     protected void encodeColOptions(final Sheet sheet, final WidgetBuilder wb)
281                 throws IOException {
282         final JavascriptVarBuilder vb = new JavascriptVarBuilder(null, false);
283         for (final SheetColumn column : sheet.getColumns()) {
284             if (!column.isRendered()) {
285                 continue;
286             }
287 
288             final JavascriptVarBuilder options = new JavascriptVarBuilder(null, true);
289             options.appendProperty("type", column.getColType(), true);
290             options.appendProperty("copyable", "true", false);
291             final Integer width = column.getColWidth();
292             String calculatedWidth = null;
293             if (width != null) {
294                 calculatedWidth = width.toString();
295             }
296             // HT doesn't have a hidden property so make column small as possible will leave
297             // it in the DOM, if 0 then Handsontable removes it entirely
298             if (!column.isVisible()) {
299                 calculatedWidth = "0.1";
300             }
301             if (calculatedWidth != null) {
302                 options.appendProperty("width", calculatedWidth, false);
303             }
304             if (column.isReadOnly()) {
305                 options.appendProperty("readOnly", "true", false);
306             }
307             options.appendProperty("trimWhitespace", column.isTrimWhitespace().toString(), false);
308             options.appendProperty("wordWrap", column.isWordWrap().toString(), false);
309 
310             // validate can be a function, regex, or string
311             final String validateFunction = column.getOnvalidate();
312             if (validateFunction != null) {
313                 final boolean quoted;
314                 switch (validateFunction) {
315                     case "autocomplete":
316                     case "date":
317                     case "numeric":
318                     case "time":
319                         quoted = true;
320                         break;
321 
322                     default:
323                         // its a function or regex!
324                         quoted = false;
325                         break;
326                 }
327                 options.appendProperty("validator", validateFunction, quoted);
328             }
329 
330             switch (column.getColType()) {
331                 case "password":
332                     final Integer passwordLength = column.getPasswordHashLength();
333                     if (passwordLength != null) {
334                         options.appendProperty("hashLength", passwordLength.toString(), false);
335                     }
336                     final String passwordSymbol = column.getPasswordHashSymbol();
337                     if (passwordSymbol != null) {
338                         options.appendProperty("hashSymbol", passwordSymbol, true);
339                     }
340                     break;
341                 case "numeric":
342                     final JavascriptVarBuilder numeric = new JavascriptVarBuilder(null, true);
343                     final String pattern = column.getNumericPattern();
344                     if (pattern != null) {
345                         numeric.appendProperty("pattern", pattern, true);
346                     }
347                     final String culture = column.getNumericLocale();
348                     if (culture != null) {
349                         numeric.appendProperty("culture", culture, true);
350                     }
351                     options.appendProperty("numericFormat", numeric.closeVar().toString(), false);
352                     break;
353                 case "date":
354                     options.appendProperty("dateFormat", column.getDateFormat(), true);
355                     options.appendProperty("correctFormat", "true", false);
356                     final String dateConfig = column.getDatePickerConfig();
357                     if (dateConfig != null) {
358                         options.appendProperty("datePickerConfig", dateConfig, false);
359                     }
360                     break;
361                 case "time":
362                     options.appendProperty("timeFormat", column.getTimeFormat(), true);
363                     options.appendProperty("correctFormat", "true", false);
364                     break;
365                 case "dropdown":
366                     encodeSelectItems(column, options);
367                     break;
368                 case "autocomplete":
369                     options.appendProperty("strict", Boolean.toString(column.isAutoCompleteStrict()), false);
370                     options.appendProperty("allowInvalid", Boolean.toString(column.isAutoCompleteAllowInvalid()),
371                                 false);
372                     options.appendProperty("trimDropdown", Boolean.toString(column.isAutoCompleteTrimDropdown()),
373                                 false);
374                     final Integer visibleRows = column.getAutoCompleteVisibleRows();
375                     if (visibleRows != null) {
376                         options.appendProperty("visibleRows", visibleRows.toString(), false);
377                     }
378                     encodeSelectItems(column, options);
379                     break;
380                 default:
381                     break;
382             }
383 
384             vb.appendArrayValue(options.closeVar().toString(), false);
385         }
386         wb.nativeAttr("columns", vb.closeVar().toString());
387     }
388 
389     private void encodeSelectItems(final SheetColumn column, final JavascriptVarBuilder options) {
390         final JavascriptVarBuilder items = new JavascriptVarBuilder(null, false);
391         final Object value = column.getSelectItems();
392         if (value == null) {
393             return;
394         }
395         if (value.getClass().isArray()) {
396             for (int j = 0; j < Array.getLength(value); j++) {
397                 final Object item = Array.get(value, j);
398                 items.appendArrayValue(String.valueOf(item), true);
399             }
400         }
401         else if (value instanceof Collection) {
402             final Collection collection = (Collection) value;
403             for (final Object item : collection) {
404                 items.appendArrayValue(String.valueOf(item), true);
405             }
406         }
407         else if (value instanceof Map) {
408             final Map map = (Map) value;
409 
410             for (final Object item : map.keySet()) {
411                 items.appendArrayValue(String.valueOf(item), true);
412             }
413         }
414 
415         options.appendProperty("source", items.closeVar().toString(), false);
416     }
417 
418     /**
419      * Encode the row data. Builds row data, style data and read only object.
420      */
421     protected void encodeData(final FacesContext context, final Sheet sheet, final WidgetBuilder wb)
422                 throws IOException {
423 
424         final JavascriptVarBuilder jsData = new JavascriptVarBuilder(null, false);
425         final JavascriptVarBuilder jsRowKeys = new JavascriptVarBuilder(null, false);
426         final JavascriptVarBuilder jsStyle = new JavascriptVarBuilder(null, true);
427         final JavascriptVarBuilder jsRowStyle = new JavascriptVarBuilder(null, false);
428         final JavascriptVarBuilder jsReadOnly = new JavascriptVarBuilder(null, true);
429         final JavascriptVarBuilder jsRowHeaders = new JavascriptVarBuilder(null, false);
430 
431         final boolean isCustomHeader = sheet.getRowHeaderValueExpression() != null;
432 
433         final List<Object> values = sheet.getSortedValues();
434         int row = 0;
435         for (final Object value : values) {
436             context.getExternalContext().getRequestMap().put(sheet.getVar(), value);
437             final String rowKey = sheet.getRowKeyValueAsString(context);
438             jsRowKeys.appendArrayValue(rowKey, true);
439             encodeRow(context, rowKey, jsData, jsRowStyle, jsStyle, jsReadOnly, sheet, row);
440 
441             // In case of custom row header evaluate the value expression for every row to
442             // set the header
443             if (sheet.isShowRowHeaders() && isCustomHeader) {
444                 final String rowHeader = sheet.getRowHeaderValueAsString(context);
445                 jsRowHeaders.appendArrayValue(rowHeader, true);
446             }
447             row++;
448         }
449 
450         sheet.setRowVar(context, null);
451 
452         wb.nativeAttr("data", jsData.closeVar().toString());
453         wb.nativeAttr("styles", jsStyle.closeVar().toString());
454         wb.nativeAttr("rowStyles", jsRowStyle.closeVar().toString());
455         wb.nativeAttr("readOnlyCells", jsReadOnly.closeVar().toString());
456         wb.nativeAttr("rowKeys", jsRowKeys.closeVar().toString());
457 
458         // add the row header as a native attribute
459         if (!isCustomHeader) {
460             wb.nativeAttr("rowHeaders", Boolean.toString(sheet.isShowRowHeaders()));
461         }
462         else {
463             wb.nativeAttr("rowHeaders", jsRowHeaders.closeVar().toString());
464         }
465     }
466 
467     /**
468      * Encode a single row.
469      *
470      * @return the JSON row
471      */
472     protected JavascriptVarBuilder encodeRow(final FacesContext context, final String rowKey,
473                 final JavascriptVarBuilder jsData, final JavascriptVarBuilder jsRowStyle,
474                 final JavascriptVarBuilder jsStyle, final JavascriptVarBuilder jsReadOnly, final Sheet sheet,
475                 final int rowIndex) {
476         // encode rowStyle (if any)
477         final String rowStyleClass = sheet.getRowStyleClass();
478         if (rowStyleClass == null) {
479             jsRowStyle.appendArrayValue("null", false);
480         }
481         else {
482             jsRowStyle.appendArrayValue(rowStyleClass, true);
483         }
484 
485         // data is array of array of data
486         final JavascriptVarBuilder jsRow = new JavascriptVarBuilder(null, false);
487         int renderCol = 0;
488         for (int col = 0; col < sheet.getColumns().size(); col++) {
489             final SheetColumn column = sheet.getColumns().get(col);
490             if (!column.isRendered()) {
491                 continue;
492             }
493 
494             // render data value
495             final String value = sheet.getRenderValueForCell(context, rowKey, col);
496             jsRow.appendArrayValue(value, true);
497 
498             // custom style
499             final String styleClass = column.getStyleClass();
500             if (styleClass != null) {
501                 jsStyle.appendRowColProperty(rowIndex, renderCol, styleClass, true);
502             }
503 
504             // read only per cell
505             final boolean readOnly = column.isReadonlyCell();
506             if (readOnly) {
507                 jsReadOnly.appendRowColProperty(rowIndex, renderCol, "true", true);
508             }
509             renderCol++;
510         }
511         // close row and append to jsData
512         jsData.appendArrayValue(jsRow.closeVar().toString(), false);
513         return jsData;
514     }
515 
516     /**
517      * Encode hidden input fields
518      */
519     private void encodeHiddenInputs(final FacesContext fc, final Sheet sheet, final String clientId)
520                 throws IOException {
521         renderHiddenInput(fc, clientId + "_input", null, false);
522         renderHiddenInput(fc, clientId + "_focus", sheet.getFocusId(), false);
523         renderHiddenInput(fc, clientId + "_selection", sheet.getSelection(), false);
524         renderHiddenInput(fc, clientId + "_sortby", Integer.toString(sheet.getSortColRenderIndex()), false);
525         renderHiddenInput(fc, clientId + "_sortorder", sheet.getSortOrder().toLowerCase(), false);
526     }
527 
528     /**
529      * Hidden accessible keyboard trap for getting focus.
530      *
531      * @param context the Faces context
532      * @param sheet the Sheet widget
533      * @throws IOException if any IO error occurs
534      * @see <a href="https://github.com/primefaces-extensions/primefaces-extensions/issues/741">GitHub 741</a>
535      */
536     protected void encodeKeyboardTrap(final FacesContext context, final Sheet sheet)
537                 throws IOException {
538         final ResponseWriter writer = context.getResponseWriter();
539         final String id = sheet.getClientId(context) + "_keyboard";
540         final String widgetVar = sheet.resolveWidgetVar(context);
541         writer.startElement("div", null);
542         writer.writeAttribute("class", "ui-helper-hidden-accessible", null);
543         writer.startElement("input", null);
544         writer.writeAttribute("id", id, null);
545         writer.writeAttribute("name", id, null);
546         writer.writeAttribute("type", "text", null);
547         writer.writeAttribute("autocomplete", "off", null);
548         writer.writeAttribute(HTML.ARIA_HIDDEN, "true", null);
549         writer.writeAttribute("tabindex", sheet.getTabindex(), null);
550         writer.writeAttribute("onfocus", String.format("PF('%s').focus();", widgetVar), null);
551         writer.endElement("input");
552         writer.endElement("div");
553     }
554 
555     /**
556      * Encode client behaviors to widget config
557      */
558     private void encodeBehaviors(final FacesContext context, final Sheet sheet, final WidgetBuilder wb)
559                 throws IOException {
560         // note we write out the onchange event here so we have the selected
561         // cell too
562         final Map<String, List<ClientBehavior>> behaviors = sheet.getClientBehaviors();
563 
564         final List<ClientBehaviorContext.Parameter> params = null;
565 
566         wb.append(",behaviors:{");
567         final String clientId = sheet.getClientId(context);
568 
569         // sort event (manual since callBack prepends leading comma)
570         if (behaviors.containsKey("sort")) {
571             final ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context,
572                         sheet, "sort", clientId, params);
573             final AjaxBehavior ajaxBehavior = (AjaxBehavior) behaviors.get("sort").get(0);
574             ajaxBehavior.setUpdate(ExtLangUtils.defaultString(ajaxBehavior.getUpdate()) + " " + clientId);
575             wb.append("sort").append(":").append("function(s, event)").append("{")
576                         .append(behaviors.get("sort").get(0).getScript(behaviorContext)).append("}");
577         }
578         else {
579             // default sort event if none defined by user
580             wb.append("sort").append(":").append("function(s, event)").append("{").append("PrimeFaces.ab({source: '")
581                         .append(clientId).append("',event: 'sort', process: '").append(clientId).append("', update: '")
582                         .append(clientId).append("'});}");
583         }
584 
585         // filter
586         if (behaviors.containsKey("filter")) {
587             final ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context,
588                         sheet, "filter", clientId, params);
589             final AjaxBehavior ajaxBehavior = (AjaxBehavior) behaviors.get("filter").get(0);
590             ajaxBehavior.setUpdate(ExtLangUtils.defaultString(ajaxBehavior.getUpdate()) + " " + clientId);
591             wb.callback("filter", "function(source, event)", behaviors.get("filter").get(0).getScript(behaviorContext));
592         }
593         else {
594             // default filter event if none defined by user
595             wb.callback("filter", "function(source, event)", "PrimeFaces.ab({s: '" + clientId
596                         + "', event: 'filter', process: '" + clientId + "', update: '" + clientId + "'});");
597         }
598 
599         if (behaviors.containsKey("change")) {
600             final ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context,
601                         sheet, "change", clientId, params);
602             wb.callback("change", "function(source, event)", behaviors.get("change").get(0).getScript(behaviorContext));
603         }
604 
605         if (behaviors.containsKey("cellSelect")) {
606             final ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context,
607                         sheet, "cellSelect", clientId, params);
608             wb.callback("cellSelect", "function(source, event)",
609                         behaviors.get("cellSelect").get(0).getScript(behaviorContext));
610         }
611 
612         if (behaviors.containsKey("columnSelect")) {
613             final ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context,
614                         sheet, "columnSelect", clientId, params);
615             wb.callback("columnSelect", "function(source, event)",
616                         behaviors.get("columnSelect").get(0).getScript(behaviorContext));
617         }
618 
619         if (behaviors.containsKey("rowSelect")) {
620             final ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(context,
621                         sheet, "rowSelect", clientId, params);
622             wb.callback("rowSelect", "function(source, event)",
623                         behaviors.get("rowSelect").get(0).getScript(behaviorContext));
624         }
625 
626         wb.append("}");
627     }
628 
629     /**
630      * Encode the sheet footer
631      */
632     private void encodeFooter(final FacesContext context, final ResponseWriter responseWriter, final Sheet sheet)
633                 throws IOException {
634         // footer
635         final UIComponent footer = sheet.getFacet("footer");
636         if (FacetUtils.shouldRenderFacet(footer)) {
637             responseWriter.startElement("div", null);
638             responseWriter.writeAttribute(Attrs.CLASS, "ui-datatable-footer ui-widget-header ui-corner-bottom", null);
639             footer.encodeAll(context);
640             responseWriter.endElement("div");
641         }
642     }
643 
644     /**
645      * Encode the Sheet header
646      */
647     private void encodeHeader(final FacesContext context, final ResponseWriter responseWriter, final Sheet sheet)
648                 throws IOException {
649         // header
650         final UIComponent header = sheet.getFacet("header");
651         if (FacetUtils.shouldRenderFacet(header)) {
652             responseWriter.startElement("div", null);
653             responseWriter.writeAttribute(Attrs.CLASS, "ui-datatable-header ui-widget-header ui-corner-top", null);
654             header.encodeAll(context);
655             responseWriter.endElement("div");
656         }
657     }
658 
659     /**
660      * Encodes the filter values.
661      */
662     protected void encodeFilterValues(final ResponseWriter responseWriter,
663                 final Sheet sheet, final String clientId) throws IOException {
664         int renderCol = 0;
665         for (final SheetColumn column : sheet.getColumns()) {
666             if (!column.isRendered()) {
667                 continue;
668             }
669 
670             if (column.getValueExpression("filterBy") != null) {
671                 responseWriter.startElement("input", null);
672                 responseWriter.writeAttribute("id", clientId + "_filter_" + renderCol, "id");
673                 responseWriter.writeAttribute("name", clientId + "_filter_" + renderCol, "name");
674                 responseWriter.writeAttribute("type", "hidden", null);
675                 responseWriter.writeAttribute("value", column.getFilterValue(), null);
676                 responseWriter.endElement("input");
677             }
678 
679             renderCol++;
680         }
681     }
682 
683     /**
684      * Encodes a javascript filter var that informs the col header event of the column's filtering options. The var is an array in the form:
685      * ["false","true",["option 1", "option 2"]] False indicates no filtering for the column. True indicates simple input text filter. Array of values indicates
686      * a drop down filter with the listed options.
687      */
688     protected void encodeFilterVar(final Sheet sheet, final WidgetBuilder wb)
689                 throws IOException {
690         final JavascriptVarBuilder vb = new JavascriptVarBuilder(null, false);
691 
692         for (final SheetColumn column : sheet.getColumns()) {
693             if (!column.isRendered()) {
694                 continue;
695             }
696 
697             if (column.getValueExpression("filterBy") == null) {
698                 vb.appendArrayValue("false", true);
699                 continue;
700             }
701 
702             final Collection<SelectItem> options = column.getFilterOptions();
703             if (options == null) {
704                 vb.appendArrayValue("true", true);
705             }
706             else {
707                 final JavascriptVarBuilder vbOptions = new JavascriptVarBuilder(null, false);
708                 for (final SelectItem item : options) {
709                     vbOptions.appendArrayValue(
710                                 "{ label: \"" + item.getLabel() + "\", value: \"" + item.getValue() + "\"}", false);
711                 }
712                 vb.appendArrayValue(vbOptions.closeVar().toString(), false);
713             }
714 
715         }
716         wb.nativeAttr("filters", vb.closeVar().toString());
717     }
718 
719     /**
720      * Encodes a javascript sort var that informs the col header event of the column's sorting options. The var is an array of boolean indicating whether or not
721      * the column is sortable.
722      */
723     protected void encodeSortVar(final Sheet sheet, final WidgetBuilder wb)
724                 throws IOException {
725         final JavascriptVarBuilder vb = new JavascriptVarBuilder(null, false);
726 
727         for (final SheetColumn column : sheet.getColumns()) {
728             if (!column.isRendered()) {
729                 continue;
730             }
731 
732             if (column.getValueExpression("sortBy") == null) {
733                 vb.appendArrayValue("false", false);
734             }
735             else {
736                 vb.appendArrayValue("true", false);
737             }
738         }
739         wb.nativeAttr("sortable", vb.closeVar().toString());
740     }
741 
742     /**
743      * Overrides decode and to parse the request parameters for the two hidden input fields:
744      * <ul>
745      * <li>clientid_input: any new changes provided by the user</li>
746      * <li>clientid_selection: the user's cell selections</li>
747      * </ul>
748      * These are JSON values and are parsed into our submitted values data on the Sheet component.
749      */
750     @Override
751     public void decode(final FacesContext context, final UIComponent component) {
752         final Sheet sheet = (Sheet) component;
753         // update Sheet references to work around issue with getParent sometimes
754         // being null
755         for (final SheetColumn column : sheet.getColumns()) {
756             column.setSheet(sheet);
757         }
758 
759         // clear updates from previous decode
760         sheet.getUpdates().clear();
761 
762         // get parameters
763         // we'll need the request parameters
764         final Map<String, String> params = context.getExternalContext().getRequestParameterMap();
765         final String clientId = sheet.getClientId(context);
766 
767         // get our input fields
768         final String jsonUpdates = params.get(clientId + "_input");
769         final String jsonSelection = params.get(clientId + "_selection");
770 
771         // decode into submitted values on the Sheet
772         decodeSubmittedValues(sheet, jsonUpdates);
773 
774         // decode the selected range so we can puke it back
775         decodeSelection(sheet, jsonSelection);
776 
777         // decode client behaviors
778         decodeBehaviors(context, sheet);
779 
780         // decode filters
781         decodeFilters(sheet, params, clientId);
782 
783         final String sortBy = params.get(clientId + "_sortby");
784         final String sortOrder = params.get(clientId + "_sortorder");
785         if (sortBy != null) {
786             int col = Integer.parseInt(sortBy);
787             if (col >= 0) {
788                 col = sheet.getMappedColumn(col);
789                 sheet.saveSortByColumn(sheet.getColumns().get(col).getId());
790             }
791         }
792 
793         if (sortOrder != null) {
794             sheet.setSortOrder(sortOrder);
795         }
796 
797         final String focus = params.get(clientId + "_focus");
798         sheet.setFocusId(focus);
799     }
800 
801     /**
802      * Decodes the filter values
803      */
804     protected void decodeFilters(final Sheet sheet, final Map<String, String> params,
805                 final String clientId) {
806         int renderCol = 0;
807         for (final SheetColumn column : sheet.getColumns()) {
808             if (!column.isRendered()) {
809                 continue;
810             }
811 
812             if (column.getValueExpression("filterBy") != null) {
813                 final String value = params.get(clientId + "_filter_" + renderCol);
814                 column.setFilterValue(value);
815             }
816 
817             renderCol++;
818         }
819     }
820 
821     /**
822      * Decodes client behaviors (ajax events).
823      *
824      * @param context the FacesContext
825      * @param component the Component being decodes
826      */
827     @Override
828     protected void decodeBehaviors(final FacesContext context, final UIComponent component) {
829 
830         // get current behaviors
831         final Map<String, List<ClientBehavior>> behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
832 
833         // if empty, done
834         if (behaviors.isEmpty()) {
835             return;
836         }
837 
838         // get the parameter map and the behaviorEvent fired
839         final Map<String, String> params = context.getExternalContext().getRequestParameterMap();
840         final String behaviorEvent = params.get("javax.faces.behavior.event");
841 
842         // if no event, done
843         if (behaviorEvent == null) {
844             return;
845         }
846 
847         // get behaviors for the event
848         final List<ClientBehavior> behaviorsForEvent = behaviors.get(behaviorEvent);
849         if (behaviorsForEvent == null || behaviorsForEvent.isEmpty()) {
850             return;
851         }
852 
853         // decode event if we are the source
854         final String behaviorSource = params.get("javax.faces.source");
855         final String clientId = component.getClientId();
856         if (clientId.equals(behaviorSource)) {
857             for (final ClientBehavior behavior : behaviorsForEvent) {
858                 behavior.decode(context, component);
859             }
860         }
861     }
862 
863     /**
864      * Decodes the user Selection JSON data
865      */
866     private void decodeSelection(final Sheet sheet, final String jsonSelection) {
867         if (LangUtils.isBlank(jsonSelection)) {
868             return;
869         }
870 
871         try {
872             // data comes in array of arrays
873             final JSONArray array = new JSONArray(jsonSelection);
874             if (array.get(0) instanceof JSONArray) {
875                 // data comes in array of arrays
876                 for (int i = 0; i < array.length(); i++) {
877                     updateSheetSelection(sheet, array.getJSONArray(i));
878                 }
879             }
880             else {
881                 // data comes in: [ [row, col, oldValue, newValue] ... ] after column/row selection
882                 updateSheetSelection(sheet, array);
883             }
884             sheet.setSelection(jsonSelection);
885         }
886         catch (final JSONException e) {
887             throw new FacesException("Failed parsing Ajax JSON message for cell selection event:" + e.getMessage(),
888                         e);
889         }
890     }
891 
892     private void updateSheetSelection(final Sheet sheet, final JSONArray array) throws JSONException {
893         // data comes in: [ [row, col, oldValue, newValue] ... ]
894         sheet.setSelectedRow(array.getInt(0));
895         sheet.setSelectedColumn(sheet.getMappedColumn(array.getInt(1)));
896         sheet.setSelectedLastRow(array.getInt(2));
897         sheet.setSelectedLastColumn(array.getInt(3));
898     }
899 
900     /**
901      * Converts the JSON data received from the in the request params into our submitted values map. The map is cleared first.
902      */
903     private void decodeSubmittedValues(final Sheet sheet, final String jsonData) {
904         if (LangUtils.isBlank(jsonData)) {
905             return;
906         }
907 
908         try {
909             // data comes in as a JSON Object with named properties for the row and columns
910             // updated this is so that
911             // multiple updates to the same cell overwrite previous deltas prior to
912             // submission we don't care about
913             // the property names, just the values, which we'll process in turn
914             final JSONObject obj = new JSONObject(jsonData);
915             final Iterator<String> keys = obj.keys();
916             while (keys.hasNext()) {
917                 final String key = keys.next();
918                 // data comes in: [row, col, oldValue, newValue, rowKey]
919                 final JSONArray update = obj.getJSONArray(key);
920                 // GitHub #586 pasted more values than rows
921                 if (update.isNull(4)) {
922                     continue;
923                 }
924                 final String rowKey = update.getString(4);
925                 final int col = sheet.getMappedColumn(update.getInt(1));
926                 final String newValue = String.valueOf(update.get(3));
927                 sheet.setSubmittedValue(rowKey, col, newValue);
928             }
929         }
930         catch (final JSONException ex) {
931             throw new FacesException("Failed parsing Ajax JSON message for cell change event:" + ex.getMessage(),
932                         ex);
933         }
934     }
935 
936     /**
937      * We render the columns (the children).
938      */
939     @Override
940     public boolean getRendersChildren() {
941         return true;
942     }
943 
944 }