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