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