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.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.Map.Entry;
35  import java.util.Set;
36  import java.util.concurrent.ConcurrentHashMap;
37  
38  import javax.el.ELContext;
39  import javax.el.ValueExpression;
40  import javax.faces.FacesException;
41  import javax.faces.application.FacesMessage;
42  import javax.faces.application.ResourceDependency;
43  import javax.faces.component.UIComponent;
44  import javax.faces.context.FacesContext;
45  import javax.faces.convert.Converter;
46  import javax.faces.convert.ConverterException;
47  import javax.faces.event.AjaxBehaviorEvent;
48  import javax.faces.event.FacesEvent;
49  
50  import org.primefaces.PrimeFaces;
51  import org.primefaces.extensions.event.SheetEvent;
52  import org.primefaces.extensions.model.sheet.SheetRowColIndex;
53  import org.primefaces.extensions.model.sheet.SheetUpdate;
54  import org.primefaces.extensions.util.ExtLangUtils;
55  import org.primefaces.extensions.util.JavascriptVarBuilder;
56  import org.primefaces.model.SortMeta;
57  import org.primefaces.model.SortOrder;
58  import org.primefaces.util.ComponentUtils;
59  import org.primefaces.util.Constants;
60  import org.primefaces.util.LangUtils;
61  
62  /**
63   * Spreadsheet component wrappering the Handsontable jQuery UI component.
64   *
65   * @author Mark Lassiter / Melloware
66   * @since 6.2
67   */
68  @ResourceDependency(library = "primefaces", name = "components.css")
69  @ResourceDependency(library = "primefaces", name = "jquery/jquery.js")
70  @ResourceDependency(library = "primefaces", name = "core.js")
71  @ResourceDependency(library = "primefaces", name = "components.js")
72  @ResourceDependency(library = "primefaces-extensions", name = "primefaces-extensions.js")
73  @ResourceDependency(library = "primefaces-extensions", target = "head", name = "sheet/sheet.css")
74  @ResourceDependency(library = "primefaces-extensions", name = "sheet/sheet.js")
75  public class Sheet extends SheetBase {
76  
77      public static final String EVENT_CELL_SELECT = "cellSelect";
78      public static final String EVENT_CHANGE = "change";
79      public static final String EVENT_SORT = "sort";
80      public static final String EVENT_FILTER = "filter";
81      public static final String EVENT_COLUMN_SELECT = "columnSelect";
82      public static final String EVENT_ROW_SELECT = "rowSelect";
83  
84      public static final String COMPONENT_TYPE = "org.primefaces.extensions.component.Sheet";
85  
86      private static final Collection<String> EVENT_NAMES = Collections.unmodifiableCollection(Arrays.asList(EVENT_CHANGE,
87                  EVENT_CELL_SELECT, EVENT_SORT, EVENT_FILTER, EVENT_COLUMN_SELECT, EVENT_ROW_SELECT));
88  
89      /**
90       * The list of UI Columns
91       */
92      private List<SheetColumn> columns;
93  
94      /**
95       * List of invalid updates
96       */
97      private List<SheetInvalidUpdate> invalidUpdates;
98  
99      /**
100      * Map of submitted values by row index and column index
101      */
102     private final Map<SheetRowColIndex, String> submittedValues = new ConcurrentHashMap<>();
103 
104     /**
105      * Map of local values by row index and column index
106      */
107     private final Map<SheetRowColIndex, Object> localValues = new ConcurrentHashMap<>();
108 
109     /**
110      * The selection data
111      */
112     private String selection;
113 
114     /**
115      * The id of the focused filter input if any
116      */
117     private String focusId;
118 
119     /**
120      * Transient list of sheet updates that can be accessed after a successful model update.
121      */
122     private final List<SheetUpdate> updates = new ArrayList<>();
123 
124     /**
125      * Maps a visible, rendered column index to the actual column based on whether or not the column is rendered. Updated on encode, and used on decode. Saved
126      * in the component state.
127      */
128     private Map<Integer, Integer> columnMapping;
129 
130     /**
131      * Map by row keys for values found in list
132      */
133     private Map<String, Object> rowMap;
134 
135     /**
136      * Map by row keys for row number
137      */
138     private Map<String, Integer> rowNumbers;
139 
140     @Override
141     public String getFamily() {
142         return SheetBase.COMPONENT_FAMILY;
143     }
144 
145     /**
146      * {@inheritDoc}
147      */
148     @Override
149     public Collection<String> getEventNames() {
150         return EVENT_NAMES;
151     }
152 
153     @Override
154     public String getDefaultEventName() {
155         return EVENT_CHANGE;
156     }
157 
158     /**
159      * {@inheritDoc}
160      */
161     @Override
162     public void queueEvent(final FacesEvent event) {
163         final FacesContext fc = FacesContext.getCurrentInstance();
164 
165         if (isSelfRequest(fc) && event instanceof AjaxBehaviorEvent) {
166             final AjaxBehaviorEvent behaviorEvent = (AjaxBehaviorEvent) event;
167             final SheetEvent sheetEvent = new SheetEvent(this, behaviorEvent.getBehavior());
168             sheetEvent.setPhaseId(event.getPhaseId());
169             super.queueEvent(sheetEvent);
170             return;
171         }
172 
173         super.queueEvent(event);
174     }
175 
176     private boolean isSelfRequest(final FacesContext context) {
177         return getClientId(context).equals(context.getExternalContext().getRequestParameterMap()
178                     .get(Constants.RequestParams.PARTIAL_SOURCE_PARAM));
179     }
180 
181     /**
182      * The list of child columns.
183      */
184     public List<SheetColumn> getColumns() {
185         if (columns == null) {
186             columns = new ArrayList<>();
187             getColumns(this);
188         }
189         return columns;
190     }
191 
192     /**
193      * The list of rendered child columns.
194      */
195     public List<SheetColumn> getRenderedColumns() {
196         final List<SheetColumn> allColumns = getColumns();
197         final List<SheetColumn> renderedCols = new ArrayList<>(allColumns.size());
198         for (final SheetColumn column : allColumns) {
199             if (column.isRendered()) {
200                 renderedCols.add(column);
201             }
202         }
203         return renderedCols;
204     }
205 
206     /**
207      * Grabs the UIColumn children for the parent specified.
208      */
209     private List<SheetColumn> getColumns(final UIComponent parent) {
210         for (final UIComponent child : parent.getChildren()) {
211             if (child instanceof SheetColumn) {
212                 columns.add((SheetColumn) child);
213             }
214         }
215         return columns;
216     }
217 
218     /**
219      * Updates the list of child columns.
220      */
221     public void setColumns(final List<SheetColumn> columns) {
222         this.columns = columns;
223     }
224 
225     /**
226      * The list of invalid updates
227      */
228     public List<SheetInvalidUpdate> getInvalidUpdates() {
229         if (invalidUpdates == null) {
230             invalidUpdates = new ArrayList<>();
231         }
232         return invalidUpdates;
233     }
234 
235     /**
236      * Resets the submitted values
237      */
238     public void resetSubmitted() {
239         submittedValues.clear();
240         updates.clear();
241     }
242 
243     /**
244      * Resets the sorting to the originally specified values (if any)
245      */
246     public void resetSort() {
247         // Set to null to restore initial sort order specified by sortBy
248         getStateHelper().put(PropertyKeys.currentSortBy.name(), null);
249 
250         final String origSortOrder = (String) getStateHelper().get(PropertyKeys.origSortOrder);
251         if (origSortOrder != null) {
252             setSortOrder(origSortOrder);
253         }
254     }
255 
256     /**
257      * Resets invalid updates
258      */
259     public void resetInvalidUpdates() {
260         getInvalidUpdates().clear();
261     }
262 
263     /**
264      * Resets all filters, sorting and submitted values.
265      */
266     public void reset() {
267         resetSubmitted();
268         resetSort();
269         resetInvalidUpdates();
270         localValues.clear();
271         for (final SheetColumn c : getColumns()) {
272             c.setFilterValue(null);
273         }
274     }
275 
276     /**
277      * Updates a submitted value.
278      */
279     public void setSubmittedValue(final String rowKey, final int col, final String value) {
280         submittedValues.put(new SheetRowColIndex(rowKey, col), value);
281     }
282 
283     /**
284      * Retrieves the submitted value for the row and col.
285      */
286     public String getSubmittedValue(final String rowKey, final int col) {
287         return submittedValues.get(new SheetRowColIndex(rowKey, col));
288     }
289 
290     /**
291      * Updates a local value.
292      */
293     public void setLocalValue(final String rowKey, final int col, final Object value) {
294         final SheetRowColIndex key = new SheetRowColIndex(rowKey, col);
295         if (value != null) {
296             localValues.put(key, value);
297         }
298         else {
299             localValues.put(key, ToBeRemoved.class);
300         }
301     }
302 
303     /**
304      * Retrieves the submitted value for the rowKey and col.
305      */
306     public Object getLocalValue(final String rowKey, final int col) {
307         return localValues.get(new SheetRowColIndex(rowKey, col));
308     }
309 
310     /**
311      * Updates the row var for iterations over the list. The var value will be updated to the value for the specified rowKey.
312      *
313      * @param context the FacesContext against which to the row var is set. Passed for performance
314      * @param rowKey the rowKey string
315      */
316     public void setRowVar(final FacesContext context, final String rowKey) {
317 
318         if (context == null) {
319             return;
320         }
321 
322         if (rowKey == null) {
323             context.getExternalContext().getRequestMap().remove(getVar());
324         }
325         else {
326             final Object value = getRowMap().get(rowKey);
327             context.getExternalContext().getRequestMap().put(getVar(), value);
328         }
329     }
330 
331     protected Map<String, Object> getRowMap() {
332         if (rowMap == null || rowMap.isEmpty()) {
333             remapRows();
334         }
335         return rowMap;
336     }
337 
338     /**
339      * Gets the object value of the row and col specified. If a local value exists, that is returned, otherwise the actual value is return.
340      */
341     public Object getValueForCell(final FacesContext context, final String rowKey, final int col) {
342         // if we have a local value, use it
343         // note: can't check for null, as null may be the submitted value
344         final SheetRowColIndex index = new SheetRowColIndex(rowKey, col);
345         if (localValues.containsKey(index)) {
346             return localValues.get(index);
347         }
348 
349         setRowVar(context, rowKey);
350         final SheetColumn column = getColumns().get(col);
351         final ValueExpression ve = column.getValueExpression("value");
352         if (ve != null) {
353             return ve.getValue(context.getELContext());
354         }
355         else {
356             return column.getValue();
357         }
358     }
359 
360     /**
361      * Gets the render string for the value the given cell. Applys the available converters to convert the value.
362      */
363     public String getRenderValueForCell(final FacesContext context, final String rowKey, final int col) {
364 
365         // if we have a submitted value still, use it
366         // note: can't check for null, as null may be the submitted value
367         final SheetRowColIndex index = new SheetRowColIndex(rowKey, col);
368         if (submittedValues.containsKey(index)) {
369             return submittedValues.get(index);
370         }
371 
372         final Object value = getValueForCell(context, rowKey, col);
373         if (value == null) {
374             return null;
375         }
376 
377         final SheetColumn column = getColumns().get(col);
378         final Converter<Object> converter = ComponentUtils.getConverter(context, column);
379         if (converter == null) {
380             return value.toString();
381         }
382         else {
383             return converter.getAsString(context, this, value);
384         }
385     }
386 
387     /**
388      * Gets the row header text value as a string for use in javascript
389      */
390     protected String getRowHeaderValueAsString(final FacesContext context) {
391         final ValueExpression veRowHeader = getRowHeaderValueExpression();
392         final Object value = veRowHeader.getValue(context.getELContext());
393         if (value == null) {
394             return Constants.EMPTY_STRING;
395         }
396         else {
397             return value.toString();
398         }
399     }
400 
401     /**
402      * The sorted list of values.
403      */
404     public List<Object> getSortedValues() {
405         List<Object> filtered = getFilteredValue();
406         if (filtered == null || filtered.isEmpty()) {
407             filtered = sortAndFilter();
408         }
409         return filtered;
410     }
411 
412     /**
413      * Gets the rendered col index of the column corresponding to the current sortBy. This is used to keep track of the current sort column in the page.
414      */
415     public int getSortColRenderIndex() {
416         // Was the column by which to sort changed by the user, ie. is there a saved ID?
417         String currentSortById = (String) getStateHelper().get(PropertyKeys.currentSortBy.name());
418         // Otherwise, did the user specify a valid column ID for the sortBy attribute?
419         if (LangUtils.isEmpty(currentSortById)) {
420             final Object sortBy = getStateHelper().eval(PropertyKeys.sortBy.name());
421             if (sortBy instanceof String) {
422                 currentSortById = (String) sortBy;
423             }
424         }
425         else {
426             int colIdx = 0;
427             for (final SheetColumn column : getColumns()) {
428                 if (!column.isRendered()) {
429                     continue;
430                 }
431 
432                 if (currentSortById.equals(column.getId())) {
433                     return colIdx;
434                 }
435                 colIdx++;
436             }
437         }
438 
439         // Otherwise, fall back to the previous behavior of searching for the column
440         // by its value expression
441         final ValueExpression veSortBy = getValueExpression(PropertyKeys.sortBy.name());
442         if (veSortBy == null) {
443             return -1;
444         }
445 
446         final String sortByExp = veSortBy.getExpressionString();
447         int colIdx = 0;
448         for (final SheetColumn column : getColumns()) {
449             if (!column.isRendered()) {
450                 continue;
451             }
452 
453             final ValueExpression veCol = column.getValueExpression(PropertyKeys.sortBy.name());
454             if (veCol != null && veCol.getExpressionString().equals(sortByExp)) {
455                 return colIdx;
456             }
457             colIdx++;
458         }
459         return -1;
460     }
461 
462     /**
463      * Evaluates the specified item value against the column filters and if they match, returns true, otherwise false. If no filterMatchMode is given on a
464      * column than the "contains" mode is used. Otherwise the following filterMatchMode values are possible: - startsWith: Checks if column value starts with
465      * the filter value. - endsWith: Checks if column value ends with the filter value. - contains: Checks if column value contains the filter value. - exact:
466      * Checks if string representations of column value and filter value are same.
467      */
468     protected boolean matchesFilter() {
469         for (final SheetColumn col : getColumns()) {
470             final String filterValue = col.getFilterValue();
471             if (LangUtils.isBlank(filterValue)) {
472                 continue;
473             }
474 
475             final Object filterBy = col.getFilterBy();
476             // if we have a filter, but no value in the row, no match
477             if (filterBy == null) {
478                 return false;
479             }
480 
481             String filterMatchMode = col.getFilterMatchMode();
482             if (LangUtils.isBlank(filterMatchMode)) {
483                 filterMatchMode = "contains";
484             }
485 
486             // case-insensitive
487             final String value = filterBy.toString().toLowerCase();
488             final String filter = filterValue.toLowerCase();
489             switch (filterMatchMode) {
490                 case "startsWith":
491                     if (!value.startsWith(filter)) {
492                         return false;
493                     }
494                     break;
495                 case "endsWith":
496                     if (!value.endsWith(filter)) {
497                         return false;
498                     }
499                     break;
500                 case "exact":
501                     if (!value.equals(filter)) {
502                         return false;
503                     }
504                     break;
505                 default:
506                     // contains is default
507                     if (!value.contains(filter)) {
508                         return false;
509                     }
510             }
511         }
512         return true;
513     }
514 
515     /**
516      * Sorts and filters the data
517      */
518     public List<Object> sortAndFilter() {
519         List filteredList = getFilteredValue();
520         if (filteredList == null) {
521             filteredList = new ArrayList();
522         }
523         filteredList.clear();
524         rowMap = new HashMap<>();
525         rowNumbers = new HashMap<>();
526 
527         final Collection<?> values = (Collection<?>) getValue();
528         if (values == null || values.isEmpty()) {
529             return filteredList;
530         }
531 
532         remapRows();
533 
534         final List<SheetColumn> columns = getRenderedColumns();
535         boolean filters = false;
536         for (final SheetColumn col : columns) {
537             if (LangUtils.isNotBlank(col.getFilterValue())) {
538                 filters = true;
539                 break;
540             }
541         }
542 
543         final FacesContext context = FacesContext.getCurrentInstance();
544         final Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
545         final String var = getVar();
546 
547         if (filters) {
548             // iterate and add those matching the filters
549             for (final Object obj : values) {
550                 requestMap.put(var, obj);
551                 if (matchesFilter()) {
552                     filteredList.add(obj);
553                 }
554             }
555         }
556         else {
557             filteredList.addAll(values);
558         }
559 
560         // Sort by the saved column. When none was saved, sort by the "sortBy" attribute of the sheet.
561         final int sortByIdx = getSortColRenderIndex();
562         final SheetColumn currentSortByColumn = sortByIdx >= 0 ? columns.get(sortByIdx) : null;
563         final ValueExpression currentSortByVe = currentSortByColumn != null ? currentSortByColumn.getValueExpression(
564                     PropertyKeys.sortBy.name()) : null;
565         final ValueExpression veSortBy;
566         if (currentSortByVe != null) {
567             veSortBy = currentSortByVe;
568         }
569         else {
570             veSortBy = getValueExpression(PropertyKeys.sortBy.name());
571         }
572         if (veSortBy != null) {
573             final SortMeta sortMeta = SortMeta.builder().field("field").caseSensitiveSort(isCaseSensitiveSort())
574                         .sortBy(veSortBy).order(convertSortOrder())
575                         .nullSortOrder(getNullSortOrder()).build();
576             filteredList.sort(new BeanPropertyComparator(var, sortMeta, Locale.ENGLISH));
577         }
578 
579         // map filtered rows
580         remapFilteredList(filteredList);
581         return filteredList;
582     }
583 
584     /**
585      * Remaps the row keys in a hash map.
586      */
587     protected void remapFilteredList(final List filteredList) {
588         rowNumbers = new HashMap<>(rowMap.size());
589         final FacesContext context = FacesContext.getCurrentInstance();
590         final Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
591         final String var = getVar();
592         int row = 0;
593         for (final Object value : filteredList) {
594             requestMap.put(var, value);
595             rowNumbers.put(getRowKeyValueAsString(context), Integer.valueOf(row));
596             row++;
597         }
598         requestMap.remove(var);
599     }
600 
601     /**
602      * Remaps the row keys in a hash map.
603      */
604     protected void remapRows() {
605         rowMap = new HashMap<>();
606         final FacesContext context = FacesContext.getCurrentInstance();
607         final Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
608         final Collection<?> values = (Collection<?>) getValue();
609         final String var = getVar();
610         for (final Object obj : values) {
611             requestMap.put(var, obj);
612             try {
613                 final String key = getRowKeyValueAsString(context);
614                 rowMap.put(key, obj);
615             }
616             finally {
617                 requestMap.remove(var);
618             }
619         }
620     }
621 
622     /**
623      * Gets the rowKey for the current row
624      *
625      * @param context the faces context
626      * @return a row key value or null if the expression is not set
627      */
628     @Override
629     protected Object getRowKeyValue(final FacesContext context) {
630         final ValueExpression veRowKey = getValueExpression(PropertyKeys.rowKey.name());
631         if (veRowKey == null) {
632             throw new FacesException("RowKey required on sheet!");
633         }
634         final Object value = veRowKey.getValue(context.getELContext());
635         if (value == null) {
636             throw new FacesException("RowKey must resolve to non-null value for updates to work properly");
637         }
638         return value;
639     }
640 
641     /**
642      * Gets the row key value as a String suitable for use in javascript rendering.
643      */
644     protected String getRowKeyValueAsString(final Object key) {
645         final String result = key.toString();
646         return "r_" + ExtLangUtils.deleteWhitespace(result);
647     }
648 
649     /**
650      * Gets the row key value as a string for the current row var.
651      */
652     protected String getRowKeyValueAsString(final FacesContext context) {
653         return getRowKeyValueAsString(getRowKeyValue(context));
654     }
655 
656     /**
657      * Convert to PF SortOrder enum since we are leveraging PF sorting code.
658      */
659     protected SortOrder convertSortOrder() {
660         final String sortOrder = getSortOrder();
661         if (sortOrder == null) {
662             return SortOrder.UNSORTED;
663         }
664         else {
665             return SortOrder.valueOf(sortOrder.toUpperCase(Locale.ENGLISH));
666         }
667     }
668 
669     /**
670      * Converts each submitted value into a local value and stores it back in the hash. If all values convert without error, then the component is valid, and we
671      * can proceed to the processUpdates.
672      */
673     @Override
674     public void validate(final FacesContext context) {
675         // iterate over submitted values and attempt to convert to the proper
676         // data type. For successful values, remove from submitted and add to
677         // local values map. for failures, add a conversion message and leave in
678         // the submitted state
679         final Iterator<Entry<SheetRowColIndex, String>> entries = submittedValues.entrySet().iterator();
680         final boolean hadBadUpdates = !getInvalidUpdates().isEmpty();
681         getInvalidUpdates().clear();
682         while (entries.hasNext()) {
683             final Entry<SheetRowColIndex, String> entry = entries.next();
684             final SheetColumn column = getColumns().get(entry.getKey().getColIndex());
685             final String newValue = entry.getValue();
686             final String rowKey = entry.getKey().getRowKey();
687             final int col = entry.getKey().getColIndex();
688             setRowVar(context, rowKey);
689 
690             // attempt to convert new value from string to correct object type
691             // based on column converter. Use PF util as helper
692             final Converter<Object> converter = ComponentUtils.getConverter(context, column);
693 
694             // assume string value if converter not found
695             Object newValueObj = newValue;
696             if (converter != null) {
697                 try {
698                     newValueObj = converter.getAsObject(context, this, newValue);
699                 }
700                 catch (final ConverterException e) {
701                     // add offending cell to list of bad updates
702                     // and to a StringBuilder for error messages (so we have one
703                     // message for the component)
704                     setValid(false);
705                     FacesMessage message = e.getFacesMessage();
706                     if (message == null) {
707                         message = new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getMessage(), e.getMessage());
708                     }
709                     context.addMessage(getClientId(context), message);
710 
711                     final String messageText = message.getDetail();
712                     getInvalidUpdates()
713                                 .add(new SheetInvalidUpdate(getRowKeyValue(context), col, column, newValue,
714                                             messageText));
715                     continue;
716                 }
717             }
718             // value is fine, no further validations (again, not to be confused
719             // with validators. until we have a "required" or something like
720             // that, nothing else to do).
721             setLocalValue(rowKey, col, newValueObj);
722 
723             // process validators on column
724             column.setValue(newValueObj);
725             try {
726                 column.validate(context);
727             }
728             finally {
729                 column.resetValue();
730             }
731 
732             entries.remove();
733         }
734         setRowVar(context, null);
735 
736         final boolean newBadUpdates = !getInvalidUpdates().isEmpty();
737         final String errorMessage = getErrorMessage();
738 
739         if ((hadBadUpdates || newBadUpdates) && context.getPartialViewContext().isPartialRequest()) {
740             // update the bad data var if partial request
741             renderBadUpdateScript();
742         }
743 
744         if (newBadUpdates && errorMessage != null) {
745             final FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, errorMessage, errorMessage);
746             context.addMessage(null, message);
747         }
748     }
749 
750     /**
751      * Override to update model with local values. Note that this is where things can be fragile in that we can successfully update some values and fail on
752      * others. There is no clean way to roll back the updates, but we also need to fail processing. Consider keeping old values as we update (need for event
753      * anyhow) and if there is a failure attempt to roll back by updating successful model updates with the old value. This may not all be necessary.
754      */
755     @Override
756     public void updateModel(final FacesContext context) {
757         final Iterator<Entry<SheetRowColIndex, Object>> entries = localValues.entrySet().iterator();
758         // Keep track of the dirtied rows for ajax callbacks so we can send
759         // updates on what was touched
760         final HashSet<String> dirtyRows = new HashSet<>();
761         while (entries.hasNext()) {
762             final Entry<SheetRowColIndex, Object> entry = entries.next();
763 
764             final Object newValue = ToBeRemoved.class.equals(entry.getValue()) ? null : entry.getValue();
765             final String rowKey = entry.getKey().getRowKey();
766             final int col = entry.getKey().getColIndex();
767             final SheetColumn column = getColumns().get(col);
768             setRowVar(context, rowKey);
769             final Object rowVal = rowMap.get(rowKey);
770 
771             final ValueExpression ve = column.getValueExpression(PropertyKeys.value.name());
772             final ELContext elContext = context.getELContext();
773             final Object oldValue = ve.getValue(elContext);
774             ve.setValue(elContext, newValue);
775             entries.remove();
776             appendUpdateEvent(getRowKeyValue(context), col, rowVal, oldValue, newValue);
777             dirtyRows.add(rowKey);
778         }
779         setLocalValueSet(false);
780         setRowVar(context, null);
781 
782         if (context.getPartialViewContext().isPartialRequest()) {
783             renderRowUpdateScript(context, dirtyRows);
784         }
785     }
786 
787     /**
788      * Saves the state of the submitted and local values and the bad updates.
789      */
790     @Override
791     public Object saveState(final FacesContext context) {
792         final Object[] values = new Object[8];
793         values[0] = super.saveState(context);
794         values[1] = submittedValues;
795         values[2] = localValues;
796         values[3] = invalidUpdates;
797         values[4] = columnMapping;
798         values[5] = getFilteredValue();
799         values[6] = rowMap;
800         values[7] = rowNumbers;
801         return values;
802     }
803 
804     /**
805      * Restores the state for the submitted, local and bad values.
806      */
807     @Override
808     public void restoreState(final FacesContext context, final Object state) {
809         if (state == null) {
810             return;
811         }
812 
813         final Object[] values = (Object[]) state;
814         super.restoreState(context, values[0]);
815         final Object restoredSubmittedValues = values[1];
816         final Object restoredLocalValues = values[2];
817         final Object restoredInvalidUpdates = values[3];
818         final Object restoredColMappings = values[4];
819         final Object restoredSortedList = values[5];
820         final Object restoredRowMap = values[6];
821         final Object restoredRowNumbers = values[7];
822 
823         submittedValues.clear();
824         if (restoredSubmittedValues != null) {
825             submittedValues.putAll((Map<SheetRowColIndex, String>) restoredSubmittedValues);
826         }
827 
828         localValues.clear();
829         if (restoredLocalValues != null) {
830             localValues.putAll((Map<SheetRowColIndex, Object>) restoredLocalValues);
831         }
832 
833         if (restoredInvalidUpdates == null) {
834             getInvalidUpdates().clear();
835         }
836         else {
837             invalidUpdates = (List<SheetInvalidUpdate>) restoredInvalidUpdates;
838         }
839 
840         if (restoredColMappings == null) {
841             columnMapping = null;
842         }
843         else {
844             columnMapping = (Map<Integer, Integer>) restoredColMappings;
845         }
846 
847         if (restoredSortedList == null) {
848             getFilteredValue().clear();
849         }
850         else {
851             setFilteredValue((List<Object>) restoredSortedList);
852         }
853 
854         if (restoredRowMap == null) {
855             rowMap = null;
856         }
857         else {
858             rowMap = (Map<String, Object>) restoredRowMap;
859         }
860 
861         if (restoredRowNumbers == null) {
862             rowNumbers = null;
863         }
864         else {
865             rowNumbers = (Map<String, Integer>) restoredRowNumbers;
866         }
867     }
868 
869     /**
870      * The selection value.
871      *
872      * @return the selection
873      */
874     public String getSelection() {
875         return selection;
876     }
877 
878     /**
879      * Updates the selection value.
880      *
881      * @param selection the selection to set
882      */
883     public void setSelection(final String selection) {
884         this.selection = selection;
885     }
886 
887     /*
888      * (non-Javadoc)
889      * @see javax.faces.component.EditableValueHolder#getSubmittedValue()
890      */
891     @Override
892     public Object getSubmittedValue() {
893         if (submittedValues.isEmpty()) {
894             return null;
895         }
896         else {
897             return submittedValues;
898         }
899     }
900 
901     /*
902      * (non-Javadoc)
903      * @see javax.faces.component.EditableValueHolder#setSubmittedValue(java.lang .Object)
904      */
905     @Override
906     public void setSubmittedValue(final Object submittedValue) {
907         submittedValues.clear();
908         if (submittedValue != null) {
909             submittedValues.putAll((Map<SheetRowColIndex, String>) submittedValue);
910         }
911 
912     }
913 
914     /**
915      * A list of updates from the last submission or ajax event.
916      *
917      * @return the editEvent
918      */
919     public List<SheetUpdate> getUpdates() {
920         return updates;
921     }
922 
923     /**
924      * Returns true if any of the columns contain conditional styling.
925      */
926     public boolean isHasStyledCells() {
927         for (final SheetColumn column : getColumns()) {
928             if (column.getStyleClass() != null) {
929                 return true;
930             }
931         }
932         return false;
933     }
934 
935     /**
936      * Maps the rendered column index to the real column index.
937      *
938      * @param renderCol the rendered index
939      * @return the mapped index
940      */
941     public int getMappedColumn(final int renderCol) {
942         if (columnMapping == null || renderCol == -1) {
943             return renderCol;
944         }
945         else {
946             final Integer result = columnMapping.get(renderCol);
947             if (result == null) {
948                 throw new IllegalArgumentException("Invalid index " + renderCol);
949             }
950             return result;
951         }
952     }
953 
954     /**
955      * Provides the render column index based on the real index
956      */
957     public int getRenderIndexFromRealIdx(final int realIdx) {
958         if (columnMapping == null || realIdx == -1) {
959             return realIdx;
960         }
961 
962         for (final Entry<Integer, Integer> entry : columnMapping.entrySet()) {
963             if (entry.getValue().equals(realIdx)) {
964                 return entry.getKey();
965             }
966         }
967 
968         return realIdx;
969     }
970 
971     /**
972      * Updates the column mappings based on the rendered attribute
973      */
974     public void updateColumnMappings() {
975         columnMapping = new HashMap<>();
976         int realIdx = 0;
977         int renderCol = 0;
978         for (final SheetColumn column : getColumns()) {
979             if (column.isRendered()) {
980                 columnMapping.put(renderCol, realIdx);
981                 renderCol++;
982             }
983             realIdx++;
984         }
985     }
986 
987     /**
988      * The number of rows in the value list.
989      */
990     public int getRowCount() {
991         return getSortedValues().size();
992     }
993 
994     /**
995      * The focusId value.
996      *
997      * @return the focusId
998      */
999     public String getFocusId() {
1000         return focusId;
1001     }
1002 
1003     /**
1004      * Updates the focusId value.
1005      *
1006      * @param focusId the focusId to set
1007      */
1008     public void setFocusId(final String focusId) {
1009         this.focusId = focusId;
1010     }
1011 
1012     /**
1013      * Invoke this method to commit the most recent set of ajax updates and restart the tracking of changes. Use this when you have processes the updates to the
1014      * model and are confident that any changes made to this point can be cleared (likely because you have persisted those changes).
1015      */
1016     public void commitUpdates() {
1017         resetSubmitted();
1018         final FacesContext context = FacesContext.getCurrentInstance();
1019         if (context.getPartialViewContext().isPartialRequest()) {
1020             final StringBuilder eval = new StringBuilder();
1021             final String jsVar = resolveWidgetVar();
1022             eval.append("PF('").append(jsVar).append("')").append(".clearDataInput();");
1023             PrimeFaces.current().executeScript(eval.toString());
1024         }
1025     }
1026 
1027     /**
1028      * Generates the bad data var value for this sheet.
1029      */
1030     public String getInvalidDataValue() {
1031         final JavascriptVarBuilder vb = new JavascriptVarBuilder(null, true);
1032         for (final SheetInvalidUpdate sheetInvalidUpdate : getInvalidUpdates()) {
1033             final Object rowKey = sheetInvalidUpdate.getInvalidRowKey();
1034             final int col = getRenderIndexFromRealIdx(sheetInvalidUpdate.getInvalidColIndex());
1035             final String rowKeyProperty = getRowKeyValueAsString(rowKey);
1036             vb.appendProperty(rowKeyProperty + "_c" + col,
1037                         sheetInvalidUpdate.getInvalidMessage().replace("'", "&apos;"), true);
1038         }
1039         return vb.closeVar().toString();
1040     }
1041 
1042     /**
1043      * Adds eval scripts to the ajax response to update the rows dirtied by the most recent successful update request.
1044      *
1045      * @param context the FacesContext
1046      * @param dirtyRows the set of dirty rows
1047      */
1048     public void renderRowUpdateScript(final FacesContext context, final Set<String> dirtyRows) {
1049         final String jsVar = resolveWidgetVar();
1050         final StringBuilder eval = new StringBuilder();
1051 
1052         for (final String rowKey : dirtyRows) {
1053             setRowVar(context, rowKey);
1054             final int rowIndex = rowNumbers.get(rowKey);
1055             // data is array of array of data
1056             final JavascriptVarBuilder jsRow = new JavascriptVarBuilder(null, false);
1057             final JavascriptVarBuilder jsStyle = new JavascriptVarBuilder(null, true);
1058             final JavascriptVarBuilder jsReadOnly = new JavascriptVarBuilder(null, true);
1059             int renderCol = 0;
1060             for (int col = 0; col < getColumns().size(); col++) {
1061                 final SheetColumn column = getColumns().get(col);
1062                 if (!column.isRendered()) {
1063                     continue;
1064                 }
1065 
1066                 // render data value
1067                 final String value = getRenderValueForCell(context, rowKey, col);
1068                 jsRow.appendArrayValue(value, true);
1069 
1070                 // custom style
1071                 final String styleClass = column.getStyleClass();
1072                 if (styleClass != null) {
1073                     jsStyle.appendRowColProperty(rowIndex, renderCol, styleClass, true);
1074                 }
1075 
1076                 // read only per cell
1077                 final boolean readOnly = column.isReadonlyCell();
1078                 if (readOnly) {
1079                     jsReadOnly.appendRowColProperty(rowIndex, renderCol, "true", true);
1080                 }
1081                 renderCol++;
1082             }
1083             eval.append("PF('").append(jsVar).append("')");
1084             eval.append(".updateData('");
1085             eval.append(rowIndex);
1086             eval.append("',");
1087             eval.append(jsRow.closeVar().toString());
1088             eval.append(",");
1089             eval.append(jsStyle.closeVar().toString());
1090             eval.append(",");
1091             eval.append(jsReadOnly.closeVar().toString());
1092             eval.append(");");
1093         }
1094         eval.append("PF('").append(jsVar).append("')").append(".redraw();");
1095         PrimeFaces.current().executeScript(eval.toString());
1096     }
1097 
1098     /**
1099      * Adds eval scripts to update the bad data array in the sheet to render validation failures produced by the most recent ajax update attempt.
1100      */
1101     public void renderBadUpdateScript() {
1102         final String widgetVar = resolveWidgetVar();
1103         final String invalidValue = getInvalidDataValue();
1104         StringBuilder sb = new StringBuilder("PF('" + widgetVar + "')");
1105         sb.append(".cfg.errors=");
1106         sb.append(invalidValue);
1107         sb.append(";");
1108         sb.append("PF('").append(widgetVar).append("')");
1109         sb.append(".ht.render();");
1110         PrimeFaces.current().executeScript(sb.toString());
1111 
1112         sb = new StringBuilder();
1113         sb.append("PF('").append(widgetVar).append("')");
1114         sb.append(".sheetDiv.removeClass('ui-state-error')");
1115         if (!getInvalidUpdates().isEmpty()) {
1116             sb.append(".addClass('ui-state-error')");
1117         }
1118         PrimeFaces.current().executeScript(sb.toString());
1119     }
1120 
1121     /**
1122      * Appends an update event
1123      */
1124     public void appendUpdateEvent(final Object rowKey, final int colIndex, final Object rowData, final Object oldValue,
1125                 final Object newValue) {
1126         updates.add(new SheetUpdate(rowKey, colIndex, rowData, oldValue, newValue));
1127     }
1128 }