Java tutorial
/* * The MIT License (MIT) * Copyright (c) 2013 Lassiter Consulting Group, LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.lassitercg.faces.components.sheet; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.el.ELContext; import javax.el.ValueExpression; import javax.faces.application.FacesMessage; import javax.faces.application.ResourceDependencies; import javax.faces.application.ResourceDependency; import javax.faces.component.EditableValueHolder; import javax.faces.component.FacesComponent; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.component.UINamingContainer; import javax.faces.component.behavior.ClientBehaviorHolder; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; import javax.faces.convert.ConverterException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.primefaces.component.api.Widget; import org.primefaces.context.RequestContext; import org.primefaces.model.BeanPropertyComparator; import org.primefaces.model.SortOrder; import org.primefaces.util.ComponentUtils; import com.lassitercg.faces.components.event.SheetUpdate; import com.lassitercg.faces.components.util.VarBuilder; /** * Spreadsheet component wrappering the Handsontable jQuery UI component. * <p> * @author <a href="mailto:mlassiter@lassitercg.com">Mark Lassiter</a> * @version $Id:$ */ @FacesComponent(value = Sheet.COMPONENTTYPE) @ResourceDependencies({ @ResourceDependency(name = "handsontable.js", target = "head", library = "handsontable"), @ResourceDependency(name = "sheet.js", target = "head", library = "handsontable"), @ResourceDependency(name = "handsontable.css", target = "head", library = "handsontable"), }) public class Sheet extends UIInput implements ClientBehaviorHolder, EditableValueHolder, Widget { public static final String EVENT_CELL_SELECT = "cellSelect"; public static final String EVENT_CHANGE = "change"; public static final String FAMILY = "com.lassitercg.faces.components"; public static final String RENDERERTYPE = "com.lassitercg.faces.components.sheet"; public static final String COMPONENTTYPE = "com.lassitercg.faces.components.sheet"; public static final String PARTIAL_SOURCE_PARAM = "javax.faces.source"; public static final String PARTIAL_BEHAVIOR_EVENT_PARAM = "javax.faces.behavior.event"; /** * Properties that are tracked by state saving. */ enum PropertyKeys { /** * <p> * The local value of this {@link UIComponent}. * </p> */ value, /** * <p> * Flag indicating whether or not this component is valid. * </p> */ valid, /** * <p> * The request scope attribute under which the data object for the * current row will be exposed when iterating. * </p> */ var, /** * The selected row */ selectedRow, /** * The last selected row */ selectedLastRow, /** * The selected column */ selectedColumn, /** * The last selected column */ selectedLastColumn, /** * flag indication whether or not to show column headers */ showColumnHeaders, /** * flag indication whether or not to show row headers */ showRowHeaders, /** * Fixed rows when scrolling */ fixedRows, /** * Fixed columns when scrolling */ fixedCols, /** * The width of the component in pixels */ width, /** * The height of the component in pixels */ height, /** * The global error message to be displayed when the sheet is in error */ errorMessage, /** * User style class for sheet */ styleClass, /** * The style class to apply to the currently selected row */ currentRowClass, /** * The style class to apply to the currently selected column */ currentColClass, /** * The row key, used to unqiuely identify each row for update operations */ rowKey, /** * The current sortBy value expression */ sortBy, /** * The current direction of the sort */ sortOrder, /** * The original sortBy value expression saved off for reset */ origSortBy, /** * The original sort direction saved off for reset */ origSortOrder, /** * The Handsontable stretchH value */ stretchH, /** * The style class to apply to each row in the sheet (EL expression) */ rowStyleClass, /** * The message displayed when no records are found */ emptyMessage } /** * The list of UI Columns */ private List<Column> columns; /** * List of bad updates */ private List<BadUpdate> badUpdates; /** * The sorted list of data */ private List<Object> sortedList; /** * Map of submitted values by row index and column index */ private Map<RowColIndex, String> submittedValues = new HashMap<RowColIndex, String>(); /** * Map of local values by row index and column index */ private Map<RowColIndex, Object> localValues = new HashMap<RowColIndex, Object>(); private Map<RowColIndex, String> comments = new HashMap<RowColIndex, String>(); /** * Current row Index for iteration operations */ private int rowIndex = -1; /** * The selection data */ private String selection; /** * The id of the focused filter input if any */ private String focusId; /** * Transient list of sheet updates that can be accessed after a successful * model update. */ private final List<SheetUpdate> updates = new ArrayList<SheetUpdate>(); /** * 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 in the component state. */ private Map<Integer, Integer> columnMapping; /** * Map by row keys for values found in list */ private Map<Object, RowMap> rowMap; /* * (non-Javadoc) * * @see javax.faces.component.UIComponent#getFamily() */ @Override public String getFamily() { return FAMILY; } /* * (non-Javadoc) * * @see javax.faces.component.UIComponentBase#getRendererType() */ @Override public String getRendererType() { return RENDERERTYPE; } /* * (non-Javadoc) * * @see javax.faces.component.UIComponentBase#getEventNames() */ @Override public Collection<String> getEventNames() { return Arrays.asList(EVENT_CHANGE, EVENT_CELL_SELECT); } /* * (non-Javadoc) * * @see javax.faces.component.UIComponentBase#getDefaultEventName() */ @Override public String getDefaultEventName() { return EVENT_CHANGE; } /** * Update's the user's custom style class to be added to the div container * for the sheet. * <p> * @param styleClass */ public void setStyleClass(String styleClass) { getStateHelper().put(PropertyKeys.styleClass, styleClass); } /** * The user's custom style class to be added to the div container for the * sheet. * <p> * @param styleClass */ public String getStyleClass() { Object result = getStateHelper().eval(PropertyKeys.styleClass, null); if (result == null) return null; return result.toString(); } /** * Update the stretcH value for the component * <p> * @param value */ public void setStretchH(String value) { getStateHelper().put(PropertyKeys.stretchH, value); } /** * The handsontable stretchH value. * <p> * @return the stretchH value */ public String getStretchH() { Object result = getStateHelper().eval(PropertyKeys.stretchH, null); if (result == null) return null; return result.toString(); } /** * Update the emptyMessage value for the component * <p> * @param value */ public void setEmptyMessage(String value) { getStateHelper().put(PropertyKeys.emptyMessage, value); } /** * The emptyMessage value. * <p> * @return the emptyMessage value */ public String getEmptyMessage() { Object result = getStateHelper().eval(PropertyKeys.emptyMessage, null); if (result == null) return null; return result.toString(); } /** * Update the current row style class * <p> * @param styleClass */ public void setCurrentColClass(String styleClass) { getStateHelper().put(PropertyKeys.currentColClass, styleClass); } /** * The col style class to use for the selected col * <p> * @param styleClass */ public String getCurrentColClass() { Object result = getStateHelper().eval(PropertyKeys.currentColClass, null); if (result == null) return null; return result.toString(); } /** * Update the current row style class * <p> * @param styleClass */ public void setCurrentRowClass(String styleClass) { getStateHelper().put(PropertyKeys.currentRowClass, styleClass); } /** * The row style class to use for the selected row * <p> * @param styleClass */ public String getCurrentRowClass() { Object result = getStateHelper().eval(PropertyKeys.currentRowClass, null); if (result == null) return null; return result.toString(); } /** * Update the current row style class to apply to the row * <p> * @param styleClass */ public void setRowStyleClass(String styleClass) { getStateHelper().put(PropertyKeys.rowStyleClass, styleClass); } /** * The row style class to apply to each row * <p> * @param styleClass */ public String getRowStyleClass() { Object result = getStateHelper().eval(PropertyKeys.rowStyleClass, null); if (result == null) return null; return result.toString(); } /** * Update the ShowColumnheaders * <p> * @param value */ public void setShowColumnHeaders(Boolean value) { getStateHelper().put(PropertyKeys.showColumnHeaders, value); } /** * Flag indicating whether or not column headers are visible * <p> * @return */ public Boolean isShowColumnHeaders() { return Boolean.valueOf(getStateHelper().eval(PropertyKeys.showColumnHeaders, true).toString()); } /** * Update the ShowRowHeaders value. * <p> * @param value */ public void setShowRowHeaders(Boolean value) { getStateHelper().put(PropertyKeys.showRowHeaders, value); } /** * The ShowRowHeaders flag * <p> * @return */ public Boolean isShowRowHeaders() { return Boolean.valueOf(getStateHelper().eval(PropertyKeys.showRowHeaders, true).toString()); } /** * The list of child columns. * <p> * @return */ public List<Column> getColumns() { if (columns == null) { columns = new ArrayList<Column>(); getColumns(this); } return columns; } /** * Grabs the UIColumn children for the parent specified. * @param parent */ private void getColumns(UIComponent parent) { for (UIComponent child : parent.getChildren()) if (child instanceof Column) columns.add((Column) child); } /** * Updates the list of child columns. * <p> * @param columns */ public void setColumns(List<Column> columns) { this.columns = columns; } /** * Updates the fixed row count. * <p> * @param value */ public void setFixedRows(Integer value) { getStateHelper().put(PropertyKeys.fixedRows, value); } /** * The fixed row count * @return */ public Integer getFixedRows() { Object result = getStateHelper().eval(PropertyKeys.fixedRows, null); if (result == null) return null; return Integer.valueOf(result.toString()); } /** * Updates the fixed columns count. * <p> * @param value */ public void setFixedCols(Integer value) { getStateHelper().put(PropertyKeys.fixedCols, value); } /** * The fixed column count. * <p> * @return */ public Integer getFixedCols() { Object result = getStateHelper().eval(PropertyKeys.fixedCols, null); if (result == null) return null; return Integer.valueOf(result.toString()); } /** * The list of bad updates * @return */ public List<BadUpdate> getBadUpdates() { if (badUpdates == null) badUpdates = new ArrayList<BadUpdate>(); return badUpdates; } /** * Resets the submitted values */ public void resetSubmitted() { this.submittedValues.clear(); } /** * Resets the sorting to the originally specified values (if any) */ public void resetSort() { ValueExpression origSortBy = (ValueExpression) getStateHelper().get(PropertyKeys.origSortBy); if (origSortBy != null) this.setSortByValueExpression(origSortBy); String origSortOrder = (String) getStateHelper().get(PropertyKeys.origSortOrder); if (origSortOrder != null) setSortOrder(origSortOrder); } /** * Resets all filters, sorting and submitted values. */ public void reset() { resetSubmitted(); resetSort(); localValues.clear(); getBadUpdates().clear(); for (Column c : getColumns()) c.setFilterValue(null); } /** * Updates a submitted value. * <p> * @param row * @param col * @param value */ public void setSubmittedValue(FacesContext context, int row, int col, String value) { // need to find row key this.setRowIndex(context, row); submittedValues.put(new RowColIndex(this.getRowKeyValue(context), col), value); } /** * Retrieves the submitted value for the row and col. * <p> * @param row * @param col * @return */ public String getSubmittedValue(Object rowKey, int col) { return submittedValues.get(new RowColIndex(rowKey, col)); } /** * Updates a local value. * <p> * @param rowKey * @param col * @param value */ public void setLocalValue(Object rowKey, int col, Object value) { localValues.put(new RowColIndex(rowKey, col), value); } /** * Retrieves the submitted value for the rowKey and col. * <p> * @param row * @param col * @return */ public Object getLocalValue(Object rowKey, int col) { return localValues.get(new RowColIndex(rowKey, col)); } public void setComment(FacesContext context, int row, int col, String comment) { this.setRowIndex(context, row); setComment(context, this.getRowKeyValue(context), col, comment); } private void setComment(FacesContext context, Object rowKey, int col, String value) { comments.put(new RowColIndex(rowKey, col), value); final Column column = getColumns().get(col); ValueExpression valueExpression = column.getValueExpression("comment"); if (valueExpression != null) { valueExpression.setValue(context.getELContext(), value); } } public String getComment(FacesContext context, Object rowKey, int col) { RowColIndex index = new RowColIndex(rowKey, col); if (!comments.containsKey(index)) { final Column column = getColumns().get(col); ValueExpression valueExpression = column.getValueExpression("comment"); return (valueExpression == null) ? null : (String) valueExpression.getValue(context.getELContext()); } return comments.get(index); } /** * The current row index for iterations over the List * @return */ public int getRowIndex() { return rowIndex; } /** * Updates the row index for iterations over the list. The var value will be * update * <p> * @param context * the FacesContext against which to the row var is set. Passed * for performance * @param rowIndex */ public void setRowIndex(FacesContext context, int rowIndex) { if (this.rowIndex != rowIndex) { this.rowIndex = rowIndex; if (context == null) return; if (rowIndex < 0) { context.getExternalContext().getRequestMap().remove(getVar()); } else { final List<Object> values = this.getSortedValues(); if (values == null) return; Object value = null; if (rowIndex < values.size()) value = values.get(rowIndex); context.getExternalContext().getRequestMap().put(getVar(), value); } } } public String getCommentForCell(FacesContext context, Object rowKey, int col) { return getComment(context, rowKey, col); } /** * Gets the object value of the row and col specified. If a local value * exists, that is returned, otherwise the actual value is return. * <p> * @param context * @param rowKey * @param col * @return */ public Object getValueForCell(FacesContext context, Object rowKey, int col) { // if we have a local value, use it // note: can't check for null, as null may be the submitted value RowColIndex index = new RowColIndex(rowKey, col); if (localValues.containsKey(index)) return localValues.get(index); RowMap map = rowMap.get(rowKey); setRowIndex(context, map.sortedIndex); final Column column = getColumns().get(col); return column.getValueExpression("value").getValue(context.getELContext()); } /** * Gets the render string for the value the given cell. Applys the available * converters to convert the value. * <p> * @param context * @param rowKey * @param col * @return */ public String getRenderValueForCell(FacesContext context, Object rowKey, int col) { // if we have a submitted value still, use it // note: can't check for null, as null may be the submitted value RowColIndex index = new RowColIndex(rowKey, col); if (submittedValues.containsKey(index)) return submittedValues.get(index); Object value = getValueForCell(context, rowKey, col); if (value == null) return null; final Column column = getColumns().get(col); Converter converter = ComponentUtils.getConverter(context, column); if (converter == null) return value.toString(); else return converter.getAsString(context, this, value); } /** * The currently selected column. * <p> * @return */ public Integer getSelectedColumn() { Object result = getStateHelper().eval(PropertyKeys.selectedColumn); if (result == null) return null; return Integer.valueOf(result.toString()); } /** * Updates the selected column. * <p> * @param col */ public void setSelectedColumn(Integer col) { getStateHelper().put(PropertyKeys.selectedColumn, col); } /** * The currently selected column. * <p> * @return */ public Integer getSelectedLastColumn() { Object result = getStateHelper().eval(PropertyKeys.selectedLastColumn); if (result == null) return null; return Integer.valueOf(result.toString()); } /** * Updates the selected column. * <p> * @param col */ public void setSelectedLastColumn(Integer col) { getStateHelper().put(PropertyKeys.selectedLastColumn, col); } /** * The currently selected row. * <p> * @return */ public Integer getSelectedRow() { Object result = getStateHelper().eval(PropertyKeys.selectedRow); if (result == null) return null; return Integer.valueOf(result.toString()); } /** * The currently selected row. * <p> * @return */ public Integer getSelectedLastRow() { Object result = getStateHelper().eval(PropertyKeys.selectedLastRow); if (result == null) return null; return Integer.valueOf(result.toString()); } /** * Updates the selected row. * <p> * @param row */ public void setSelectedRow(Integer row) { getStateHelper().put(PropertyKeys.selectedRow, row); } /** * Updates the selected row. * <p> * @param row */ public void setSelectedLastRow(Integer row) { getStateHelper().put(PropertyKeys.selectedLastRow, row); } /** * The width of the sheet in pixels * <p> * @return */ public Integer getWidth() { Object result = getStateHelper().eval(PropertyKeys.width); if (result == null) return null; // this will handle any type so long as its convertable to integer return Integer.valueOf(result.toString()); } /** * Updates the width * <p> * @param row */ public void setWidth(Integer value) { getStateHelper().put(PropertyKeys.width, value); } /** * The height of the sheet. Note this is applied to the inner div which is * why it is recommend you use this property instead of a style class. * <p> * @return */ public Integer getHeight() { Object result = getStateHelper().eval(PropertyKeys.height); if (result == null) return null; // this will handle any type so long as its convertable to integer return Integer.valueOf(result.toString()); } /** * Updates the height * <p> * @param row */ public void setHeight(Integer value) { getStateHelper().put(PropertyKeys.height, value); } /** * <p> * Return the value of the Sheet. This value must be a java.util.List value * at this time. * </p> */ @Override public Object getValue() { return getStateHelper().eval(PropertyKeys.value); } /** * The sorted list of values. * <p> * @return */ public List<Object> getSortedValues() { if (sortedList == null) sortAndFilter(); return sortedList; } /** * 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. * <p> * @return */ public int getSortColRenderIndex() { ValueExpression veSortBy = getValueExpression(PropertyKeys.sortBy.name()); if (veSortBy == null) return -1; final String sortByExp = veSortBy.getExpressionString(); int colIdx = 0; for (Column column : getColumns()) { if (!column.isRendered()) continue; ValueExpression veCol = column.getValueExpression(PropertyKeys.sortBy.name()); if (veCol != null) { if (veCol.getExpressionString().equals(sortByExp)) return colIdx; } colIdx++; } return -1; } /** * Evaluates the specified item value against the column filters and if they * match, returns true, otherwise false. * <p> * @param obj * @return */ protected boolean matchesFilter(Object obj) { for (Column col : getColumns()) { String filterValue = col.getFilterValue(); if (StringUtils.isEmpty(filterValue)) continue; Object filterBy = col.getFilterBy(); // if we have a filter, but no value in the row, no match if (filterBy == null) return false; // case-insensitive String compareA = filterBy.toString().toLowerCase(); String compareB = filterValue.toLowerCase(); // TODO need to support match modes if (!compareA.contains(compareB)) return false; } return true; } /** * Sorts and filters the data */ @SuppressWarnings("unchecked") public void sortAndFilter() { sortedList = new ArrayList<Object>(); rowMap = new HashMap<Object, RowMap>(); Collection<?> values = (Collection<?>) getValue(); if (values == null || values.isEmpty()) return; boolean filters = false; for (Column col : getColumns()) if (StringUtils.isNotEmpty(col.getFilterValue())) { filters = true; break; } FacesContext context = FacesContext.getCurrentInstance(); Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); if (filters) { // iterate and add those matching the filters String var = getVar(); for (Object obj : values) { requestMap.put(var, obj); try { if (matchesFilter(obj)) sortedList.add(obj); } finally { requestMap.remove(var); } } } else sortedList.addAll(values); ValueExpression veSortBy = this.getValueExpression(PropertyKeys.sortBy.name()); if (veSortBy != null) Collections.sort(sortedList, new BeanPropertyComparator(veSortBy, getVar(), convertSortOrder(), null)); reMapRows(); } /** * Remaps the row keys to the sorted and filtered list. */ protected void reMapRows() { FacesContext context = FacesContext.getCurrentInstance(); Map<String, Object> requestMap = context.getExternalContext().getRequestMap(); for (int i = 0; i < sortedList.size(); i++) { Object obj = sortedList.get(i); String var = getVar(); requestMap.put(var, obj); try { RowMap map = new RowMap(); map.sortedIndex = i; map.value = obj; rowMap.put(getRowKeyValue(context), map); } finally { requestMap.remove(var); } } } /** * Gets the rowKey for the current row * <p> * @param context * the faces context * @return a row key value or null if the expression is not set */ protected Object getRowKeyValue(FacesContext context) { ValueExpression veRowKey = getValueExpression(PropertyKeys.rowKey.name()); if (veRowKey == null) throw new RuntimeException("RowKey required on sheet!"); Object value = veRowKey.getValue(context.getELContext()); if (value == null) throw new RuntimeException("RowKey must resolve to non-null valkue for updates to work properly"); return value; } /** * Convert to PF SortOrder enum since we are leveraging PF sorting code. * <p> * @return */ protected SortOrder convertSortOrder() { String sortOrder = getSortOrder(); if (sortOrder == null) return SortOrder.UNSORTED; else { SortOrder result = SortOrder.valueOf(sortOrder.toUpperCase(Locale.ENGLISH)); return result; } } /** * <p> * Set the value of the <code>Sheet</code>. This value must be a * java.util.List at this time. * </p> * @param value * the new value */ @Override public void setValue(Object value) { getStateHelper().put(PropertyKeys.value, value); } /** * <p> * Return the request-scope attribute under which the data object for the * current row will be exposed when iterating. This property is * <strong>not</strong> enabled for value binding expressions. * </p> */ public String getVar() { // must be a string literal (no eval) return (String) getStateHelper().get(PropertyKeys.var); } /** * <p> * Set the request-scope attribute under which the data object for the * current row wil be exposed when iterating. * </p> * @param var * The new request-scope attribute name */ public void setVar(String var) { getStateHelper().put(PropertyKeys.var, var); } /** * The current sortBy value expression in use. * @return */ public ValueExpression getSortByValueExpression() { ValueExpression veSortBy = getValueExpression(PropertyKeys.sortBy.name()); return veSortBy; } /** * Update the sort field * @param sortBy */ public void setSortByValueExpression(ValueExpression sortBy) { // when updating, make sure we store off the original so it may be // restored ValueExpression orig = (ValueExpression) getStateHelper().get(PropertyKeys.origSortBy); if (orig == null) { getStateHelper().put(PropertyKeys.origSortBy, getSortByValueExpression()); } setValueExpression(PropertyKeys.sortBy.name(), sortBy); } /** * The sort direction * @return */ public String getSortOrder() { // if we have a toggled sort in our state, use it String result = (String) getStateHelper().eval(PropertyKeys.sortOrder, SortOrder.ASCENDING.toString()); return result; } /** * Update the sort direction * @param sortOrder */ public void setSortOrder(java.lang.String sortOrder) { // when updating, make sure we store off the original so it may be // restored String orig = (String) getStateHelper().get(PropertyKeys.origSortOrder); if (orig == null) // do not call getSortOrder as it defaults to ascending, we want // null // if this is the first call and there is no previous value. getStateHelper().put(PropertyKeys.origSortOrder, getStateHelper().eval(PropertyKeys.sortOrder)); getStateHelper().put(PropertyKeys.sortOrder, sortOrder); } /** * The error message to display when the sheet is in error. * <p> * @return */ public String getErrorMessage() { Object result = getStateHelper().eval(PropertyKeys.errorMessage); if (result == null) return null; return result.toString(); } /** * Updates the errorMessage value. * @param value */ public void setErrorMessage(String value) { getStateHelper().put(PropertyKeys.errorMessage, value); } /* * (non-Javadoc) * * @see javax.faces.component.UIInput#processValidators(javax.faces.context. * FacesContext) */ @Override public void processValidators(FacesContext context) { super.processValidators(context); } /** * 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 can proceed to the processUpdates. */ @Override public void validate(FacesContext context) { Iterator<Entry<RowColIndex, String>> entries = submittedValues.entrySet().iterator(); boolean hadBadUpdates = !getBadUpdates().isEmpty(); getBadUpdates().clear(); while (entries.hasNext()) { final Entry<RowColIndex, String> entry = entries.next(); final Column column = getColumns().get(entry.getKey().colIndex); final String newValue = entry.getValue(); final Object rowKey = entry.getKey().getRowKey(); final int col = entry.getKey().getColIndex(); final RowMap map = rowMap.get(rowKey); this.setRowIndex(context, map.sortedIndex); // attempt to convert new value from string to correct object type // based on column converter. Use PF util as helper Converter converter = ComponentUtils.getConverter(context, column); // assume string value if converter not found Object newValueObj = newValue; if (converter != null) try { newValueObj = converter.getAsObject(context, this, newValue); } catch (ConverterException e) { // add offending cell to list of bad updates // and to a stringbuffer for error messages (so we have one // message for the component) setValid(false); FacesMessage message = e.getFacesMessage(); if (message == null) { message = new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getMessage(), e.getMessage()); } context.addMessage(this.getClientId(context), message); String messageText = message.getDetail(); this.getBadUpdates() .add(new BadUpdate(getRowKeyValue(context), col, column, newValue, messageText)); continue; } // value is fine, no further validations (again, not to be confused // with validators. until we have a "required" or something like // that, nothing else to do). setLocalValue(rowKey, col, newValueObj); // process validators on column column.setValue(newValueObj); try { column.validate(context); } finally { column.resetValue(); } entries.remove(); } this.setRowIndex(context, -1); final boolean newBadUpdates = !getBadUpdates().isEmpty(); String errorMessage = this.getErrorMessage(); if (hadBadUpdates || newBadUpdates) { // update the bad data var if partial request if (context.getPartialViewContext().isPartialRequest()) { this.sortAndFilter(); this.renderBadUpdateScript(context); } } if (newBadUpdates && errorMessage != null) { FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, errorMessage, errorMessage); context.addMessage(null, message); } } /** * 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 others. There is no clean way to roll back the updates, but we * also need to fail processing. * <p> * TODO consider keeping old values as we update (need for event 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. */ @Override public void updateModel(FacesContext context) { HashSet<Object> dirtyRows = new HashSet<Object>(); Iterator<Entry<RowColIndex, String>> commentsEntries = comments.entrySet().iterator(); while (commentsEntries.hasNext()) { final Entry<RowColIndex, String> entry = commentsEntries.next(); final String newValue = entry.getValue(); final Object rowKey = entry.getKey().getRowKey(); final int col = entry.getKey().getColIndex(); final Column column = getColumns().get(col); final RowMap map = rowMap.get(rowKey); String oldValue = null; ValueExpression valueExpression = column.getValueExpression("comment"); if (valueExpression != null) { oldValue = (String) valueExpression.getValue(context.getELContext()); } commentsEntries.remove(); if (!StringUtils.equals(oldValue, newValue)) { appendUpdateEvent(map.sortedIndex, col, map.value, oldValue, newValue); dirtyRows.add(rowKey); } } Iterator<Entry<RowColIndex, Object>> entries = localValues.entrySet().iterator(); // Keep track of the dirtied rows for ajax callbacks so we can send // updates on what was touched while (entries.hasNext()) { final Entry<RowColIndex, Object> entry = entries.next(); final Object newValue = entry.getValue(); final Object rowKey = entry.getKey().getRowKey(); final int col = entry.getKey().getColIndex(); final Column column = getColumns().get(col); final RowMap map = rowMap.get(rowKey); this.setRowIndex(context, map.sortedIndex); // System.out.println("Local key=" + rowKey + " and sortedRow is " + map.sortedIndex); ValueExpression ve = column.getValueExpression(PropertyKeys.value.name()); ELContext elContext = context.getELContext(); Object oldValue = ve.getValue(elContext); if (!column.isReadonlyCell()) { ve.setValue(elContext, newValue); } entries.remove(); appendUpdateEvent(map.sortedIndex, col, map.value, oldValue, newValue); dirtyRows.add(rowKey); } setLocalValueSet(false); setRowIndex(context, -1); this.sortAndFilter(); if (context.getPartialViewContext().isPartialRequest()) this.renderRowUpdateScript(context, dirtyRows); } /** * Saves the state of the submitted and local values and the bad updates. */ @Override public Object saveState(FacesContext context) { Object values[] = new Object[7]; values[0] = super.saveState(context); values[1] = submittedValues; values[2] = localValues; values[3] = badUpdates; values[4] = columnMapping; values[5] = sortedList; values[6] = rowMap; return values; } /** * Restores the state for the submitted, local and bad values. */ @SuppressWarnings("unchecked") @Override public void restoreState(FacesContext context, Object state) { if (state == null) return; Object values[] = (Object[]) state; super.restoreState(context, values[0]); Object restoredSubmittedValues = values[1]; Object restoredLocalValues = values[2]; Object restoredBadUpdates = values[3]; Object restoredColMappings = values[4]; Object restoredSortedList = values[5]; Object restoredRowMap = values[6]; if (restoredSubmittedValues == null) submittedValues.clear(); else submittedValues = (Map<RowColIndex, String>) restoredSubmittedValues; if (restoredLocalValues == null) localValues.clear(); else localValues = (Map<RowColIndex, Object>) restoredLocalValues; if (restoredBadUpdates == null) { if (badUpdates != null) badUpdates.clear(); } else { badUpdates = (List<BadUpdate>) restoredBadUpdates; } if (restoredColMappings == null) columnMapping = null; else columnMapping = (Map<Integer, Integer>) restoredColMappings; if (restoredSortedList == null) sortedList = null; else sortedList = (List<Object>) restoredSortedList; if (restoredRowMap == null) rowMap = null; else rowMap = (Map<Object, RowMap>) restoredRowMap; } /** * The selection value. * <p> * @return the selection */ public String getSelection() { return selection; } /** * Updates the selection value. * <p> * @param selection * the selection to set */ public void setSelection(String selection) { this.selection = selection; } /* * (non-Javadoc) * * @see javax.faces.component.EditableValueHolder#getSubmittedValue() */ @Override public Object getSubmittedValue() { if (this.submittedValues.isEmpty()) return null; else return (this.submittedValues); } /* * (non-Javadoc) * * @see * javax.faces.component.EditableValueHolder#setSubmittedValue(java.lang * .Object) */ @SuppressWarnings("unchecked") @Override public void setSubmittedValue(Object submittedValue) { if (submittedValue == null) submittedValues.clear(); else submittedValues = (Map<RowColIndex, String>) submittedValue; } /** * A list of updates from the last submission or ajax event. * <p> * @return the editEvent */ public List<SheetUpdate> getUpdates() { return updates; } /** * Appends an update event * <p> * @param rowIndex * @param colIndex * @param rowData * @param oldValue * @param newValue */ protected void appendUpdateEvent(int rowIndex, int colIndex, Object rowData, Object oldValue, Object newValue) { updates.add(new SheetUpdate(rowIndex, colIndex, rowData, oldValue, newValue)); } /** * Returns true if any of the columns contain conditional styling. * <p> * @return */ public boolean isHasStyledCells() { for (Column column : getColumns()) if (column.getStyleClass() != null) return true; return false; } /** * Maps the rendered column index to the real column index. * <p> * @param renderIdx * the rendered index * @return the mapped index */ public int getMappedColumn(int renderIdx) { if (columnMapping == null) { return renderIdx; } else { Integer result = columnMapping.get(renderIdx); if (result == null) throw new IllegalArgumentException("Invalid index " + renderIdx); return result; } } /** * Provides the render column index based on the real index * @param realIdx * @return */ public int getRenderIndexFromRealIdx(int realIdx) { if (columnMapping == null) { return realIdx; } for (Entry<Integer, Integer> entry : columnMapping.entrySet()) if (entry.getValue().equals(realIdx)) return entry.getKey(); return realIdx; } /** * Updates the column mappings based on the rendered attribute */ public void updateColumnMappings() { columnMapping = new HashMap<Integer, Integer>(); int realIdx = 0; int renderIdx = 0; for (Column column : getColumns()) { if (column.isRendered()) { columnMapping.put(renderIdx, realIdx); renderIdx++; } realIdx++; } } /** * The number of rows in the value list. * <p> * @return */ public int getRowCount() { List<Object> values = getSortedValues(); if (values == null) return 0; return values.size(); } /** * The focusId value. * <p> * @return the focusId */ public String getFocusId() { return focusId; } /** * Updates the focusId value. * <p> * @param focusId * the focusId to set */ public void setFocusId(String focusId) { this.focusId = focusId; } /** * 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 model and are confident that any changes made to this * point can be cleared (likely because you have persisted those changes). */ public void commitUpdates() { resetSubmitted(); FacesContext context = FacesContext.getCurrentInstance(); if (context.getPartialViewContext().isPartialRequest()) { StringBuffer eval = new StringBuffer(); String jQueryId = this.getClientId().replace(":", "\\\\:"); String jsDeltaVar = this.getClientId().replace(":", "_") + "_delta"; eval.append("$('#"); eval.append(jQueryId); eval.append("_input').val('');"); eval.append(jsDeltaVar); eval.append("={};"); RequestContext.getCurrentInstance().getScriptsToExecute().add(eval.toString()); } } /** * Generates the bad data var value for this sheet. * <p> * @param sheet * @param badDataVar * @return */ public String getBadDataValue() { VarBuilder vb = new VarBuilder(null, true); for (BadUpdate badUpdate : getBadUpdates()) { final Object rowKey = badUpdate.getBadRowKey(); final int col = getRenderIndexFromRealIdx(badUpdate.getBadColIndex()); RowMap map = rowMap.get(rowKey); System.out.println("RowMap is " + map.sortedIndex + " for key " + rowKey); vb.appendRowColProperty(map.sortedIndex, col, badUpdate.getBadMessage().replace("'", "'"), true); } return vb.closeVar().toString(); } public void updateDirtyRows(Set<Object> dirtyRows) { FacesContext context = FacesContext.getCurrentInstance(); renderRowUpdateScript(context, dirtyRows); setRowIndex(context, -1); } /** * Adds eval scripts to the ajax response to update the rows dirtied by the * most recent successful update request. * <p> * @param context * the FacesContext * @param dirtyRows * the set of dirty rows */ protected void renderRowUpdateScript(FacesContext context, Set<Object> dirtyRows) { String jsVar = this.resolveWidgetVar(); StringBuilder eval = new StringBuilder(); for (Object rowKey : dirtyRows) { RowMap map = this.rowMap.get(rowKey); setRowIndex(context, map.sortedIndex); // data is array of array of data VarBuilder vbRow = new VarBuilder(null, false); for (int col = 0; col < getColumns().size(); col++) { final Column column = getColumns().get(col); if (!column.isRendered()) continue; String value = getRenderValueForCell(context, rowKey, col); vbRow.appendArrayValue(value, true); String styleClass = column.getStyleClass(); if (styleClass != null) { eval.append(jsVar); eval.append(".cfg.styles['r"); eval.append(rowIndex); eval.append("_c"); eval.append(col); eval.append("']='"); eval.append(styleClass); eval.append("';"); } } eval.append(jsVar); eval.append(".cfg.data["); eval.append(Integer.toString(map.sortedIndex)); eval.append("]="); eval.append(vbRow.closeVar().toString()); eval.append(";"); eval.append(jsVar); eval.append(";"); } eval.append(jsVar); eval.append(".ht.render();"); RequestContext.getCurrentInstance().getScriptsToExecute().add(eval.toString()); } /** * Adds eval scripts to update the bad data array in the sheet to render * valdiation failures produced by the most recent ajax update attempt. * <p> * @param context * the FacesContext */ protected void renderBadUpdateScript(FacesContext context) { String widgetVar = this.resolveWidgetVar(); String badDataVar = this.getBadDataValue(); StringBuffer sb = new StringBuffer(widgetVar); sb.append(".cfg.errors="); sb.append(badDataVar); sb.append(";"); sb.append(widgetVar); sb.append(".ht.render();"); RequestContext.getCurrentInstance().getScriptsToExecute().add(sb.toString()); sb = new StringBuffer(); sb.append(widgetVar); sb.append(".sheetDiv.removeClass('ui-state-error')"); if (!getBadUpdates().isEmpty()) sb.append(".addClass('ui-state-error')"); RequestContext.getCurrentInstance().getScriptsToExecute().add(sb.toString()); } /* * (non-Javadoc) * * @see org.primefaces.component.api.Widget#resolveWidgetVar() */ @Override public String resolveWidgetVar() { FacesContext context = FacesContext.getCurrentInstance(); String userWidgetVar = (String) getAttributes().get("widgetVar"); if (userWidgetVar != null) return userWidgetVar; else return "widget_" + getClientId(context).replaceAll("-|" + UINamingContainer.getSeparatorChar(context), "_"); } /* * Private class used as a key for row,col maps. */ private class RowColIndex implements Serializable { private static final long serialVersionUID = 1L; private final Object rowKey; private final Integer colIndex; /** * Constructs an instance of RowColIndex for the row and column * specified. * <p> * @param row * the row represented by this index * @param col * the column respresented by this index */ public RowColIndex(Object rowKey, Integer col) { this.rowKey = rowKey; this.colIndex = col; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object other) { if (!(other instanceof RowColIndex)) return false; RowColIndex castOther = (RowColIndex) other; return new EqualsBuilder().append(rowKey, castOther.rowKey).append(colIndex, castOther.colIndex) .isEquals(); } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return new HashCodeBuilder().append(rowKey).append(colIndex).toHashCode(); } /** * The rowIndex value. * <p> * @return the rowIndex */ public Object getRowKey() { return rowKey; } /** * The colIndex value. * <p> * @return the colIndex */ public Integer getColIndex() { return colIndex; } } /* * Private class used to map a row key to its object and sorted row index */ private class RowMap implements Serializable { private static final long serialVersionUID = 1L; Object value; int sortedIndex; } }