diff --git a/src/main/java/net/bootsfaces/component/dataTable/DataTable.java b/src/main/java/net/bootsfaces/component/dataTable/DataTable.java index 250df9e69..6cc4c2242 100644 --- a/src/main/java/net/bootsfaces/component/dataTable/DataTable.java +++ b/src/main/java/net/bootsfaces/component/dataTable/DataTable.java @@ -18,29 +18,39 @@ package net.bootsfaces.component.dataTable; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import javax.el.ValueExpression; +import javax.faces.application.FacesMessage; import javax.faces.application.ResourceDependencies; import javax.faces.application.ResourceDependency; import javax.faces.component.FacesComponent; +import javax.faces.component.UIColumn; +import javax.faces.component.UIComponent; +import javax.faces.component.UIComponentBase; +import javax.faces.component.UIData; import javax.faces.component.behavior.ClientBehaviorHolder; +import javax.faces.context.FacesContext; +import javax.faces.event.PhaseId; import net.bootsfaces.component.ajax.IAJAXComponent; +import net.bootsfaces.component.dataTableColumn.DataTableColumn; import net.bootsfaces.listeners.AddResourcesListener; import net.bootsfaces.render.IResponsive; import net.bootsfaces.render.Tooltip; import net.bootsfaces.utils.BsfUtils; /** This class holds the attributes of <b:dataTable />. */ -@ResourceDependencies({ - @ResourceDependency(library = "bsf", name = "js/datatables.min.js", target = "body"), -// @ResourceDependency(library = "bsf", name = "js/datatables-bf-extensions.js", target = "body"), +@ResourceDependencies({ @ResourceDependency(library = "bsf", name = "js/datatables.min.js", target = "body"), + // @ResourceDependency(library = "bsf", name = + // "js/datatables-bf-extensions.js", target = "body"), @ResourceDependency(library = "bsf", name = "css/datatables.min.css", target = "head") }) @FacesComponent("net.bootsfaces.component.dataTable.DataTable") public class DataTable extends DataTableCore @@ -55,17 +65,20 @@ public class DataTable extends DataTableCore private static final Collection EVENT_NAMES = Collections.unmodifiableCollection(Arrays.asList("click", "dblclick", "dragstart", "dragover", "drop", "mousedown", "mousemove", "mouseout", "mouseover", "mouseup")); - /** This map is used to store the column sort information gathered during rendering. */ + /** + * This map is used to store the column sort information gathered during + * rendering. + */ private Map columnSortOrder; - /** This array ist used to store the column information bits that are used to initialize the columns using the - * columns attribute of datatables.net */ + /** + * This array ist used to store the column information bits that are used to + * initialize the columns using the columns attribute of datatables.net + */ private List columnInfo; public enum DataTablePropertyType { - pageLength, - searchTerm, - currentPage + pageLength, searchTerm, currentPage } public DataTable() { @@ -115,8 +128,8 @@ public String getFamily() { } /** - * This map contains all of the default sorting for each column. - * This map is used to store the column sort information gathered during rendering. + * This map contains all of the default sorting for each column. This map is + * used to store the column sort information gathered during rendering. * * @return The map containing the column / sort type pairs */ @@ -125,22 +138,307 @@ public Map getColumnSortOrderMap() { } /** - * Called in order to lazily initialize the map. - * This map is used to store the column sort information gathered during rendering. + * Called in order to lazily initialize the map. This map is used to store + * the column sort information gathered during rendering. */ public void initColumnSortOrderMap() { this.columnSortOrder = new HashMap(); } - /** This array ist used to store the column information bits that are used to initialize the columns using the - * columns attribute of datatables.net */ + /** + * This array ist used to store the column information bits that are used to + * initialize the columns using the columns attribute of datatables.net + */ public List getColumnInfo() { return columnInfo; } - /** This array ist used to store the column information bits that are used to initialize the columns using the - * columns attribute of datatables.net */ + /** + * This array ist used to store the column information bits that are used to + * initialize the columns using the columns attribute of datatables.net + */ public void setColumnInfo(List columnInfo) { this.columnInfo = columnInfo; } + + /** + *

+ * Flag indicating whether or not this UIData instance is nested within + * another UIData instance + *

+ * + *

+ * This is not part of the component state. + *

+ */ + private Boolean isNested = null; + + enum PKFromUIData { + /** + *

+ * This map contains SavedState instances for each + * descendant component, keyed by the client identifier of the + * descendant. Because descendant client identifiers will contain the + * rowIndex value of the parent, per-row state information + * is actually preserved. + *

+ */ + saved + } + + /** + *

+ * Override the default {@link UIComponentBase#processDecodes} processing to + * perform the following steps. + *

+ *
    + *
  • If the rendered property of this {@link UIComponent} is + * false, skip further processing.
  • + *
  • Set the current rowIndex to -1.
  • + *
  • Call the processDecodes() method of all facets of this + * {@link UIData}, in the order determined by a call to + * getFacets().keySet().iterator().
  • + *
  • Call the processDecodes() method of all facets of the + * {@link UIColumn} children of this {@link UIData}.
  • + *
  • Iterate over the set of rows that were included when this component + * was rendered (i.e. those defined by the first and + * rows properties), performing the following processing for + * each row: + *
      + *
    • Set the current rowIndex to the appropriate value for + * this row.
    • + *
    • If isRowAvailable() returns true, iterate + * over the children components of each {@link UIColumn} child of this + * {@link UIData} component, calling the processDecodes() + * method for each such child.
    • + *
    + *
  • + *
  • Set the current rowIndex to -1.
  • + *
  • Call the decode() method of this component.
  • + *
  • If a RuntimeException is thrown during decode + * processing, call {@link FacesContext#renderResponse} and re-throw the + * exception.
  • + *
+ * + * @param context + * {@link FacesContext} for the current request + * + * @throws NullPointerException + * if context is null + */ + public void processDecodes(FacesContext context) { + + if (context == null) { + throw new NullPointerException(); + } + if (!isRendered()) { + return; + } + + pushComponentToEL(context, this); + preDecode(context); + iterate(context, PhaseId.APPLY_REQUEST_VALUES); + decode(context); + popComponentFromEL(context); + } + + // Perform pre-decode initialization work. Note that this + // initialization may be performed either during a normal decode + // (ie. processDecodes()) or during a tree visit (ie. visitTree()). + private void preDecode(FacesContext context) { + setDataModel(null); // Re-evaluate even with server-side state saving + /** + * Properties that are tracked by state saving. + */ + + Object saved = getStateHelper().get(PKFromUIData.saved); + if (null == saved || !keepSaved(context)) { + // noinspection CollectionWithoutInitialCapacity + getStateHelper().remove(PKFromUIData.saved); + } + } + + /** + *

+ * Perform the appropriate phase-specific processing and per-row iteration + * for the specified phase, as follows: + *

    + *
  • Set the rowIndex property to -1, and process the facets + * of this {@link UIData} component exactly once.
  • + *
  • Set the rowIndex property to -1, and process the facets + * of the {@link UIColumn} children of this {@link UIData} component exactly + * once.
  • + *
  • Iterate over the relevant rows, based on the first and + * row properties, and process the children of the + * {@link UIColumn} children of this {@link UIData} component once per + * row.
  • + *
+ * + * @param context + * {@link FacesContext} for the current request + * @param phaseId + * {@link PhaseId} of the phase we are currently running + */ + private void iterate(FacesContext context, PhaseId phaseId) { + + // Process each facet of this component exactly once + setRowIndex(-1); + if (getFacetCount() > 0) { + for (UIComponent facet : getFacets().values()) { + if (phaseId == PhaseId.APPLY_REQUEST_VALUES) { + facet.processDecodes(context); + } else if (phaseId == PhaseId.PROCESS_VALIDATIONS) { + facet.processValidators(context); + } else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { + facet.processUpdates(context); + } else { + throw new IllegalArgumentException(); + } + } + } + + // collect rendered columns once + List renderedColumns = new ArrayList(getChildCount()); + if (getChildCount() > 0) { + for (UIComponent child : getChildren()) { + if (child instanceof UIColumn && child.isRendered()) { + renderedColumns.add(child); + } else if (child instanceof DataTableColumn && child.isRendered()) { + renderedColumns.add(child); + } + } + } + + // Process each facet of our child UIColumn components exactly once + setRowIndex(-1); + for (UIComponent column : renderedColumns) { + if (column.getFacetCount() > 0) { + + for (UIComponent columnFacet : column.getFacets().values()) { + resetClientIdCacheRecursively(columnFacet); + if (phaseId == PhaseId.APPLY_REQUEST_VALUES) { + columnFacet.processDecodes(context); + } else if (phaseId == PhaseId.PROCESS_VALIDATIONS) { + columnFacet.processValidators(context); + } else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { + columnFacet.processUpdates(context); + } else { + throw new IllegalArgumentException(); + } + } + } + } + + // Iterate over our UIColumn children, once per row + int processed = 0; + int rowIndex = getFirst() - 1; + int rows = getRows(); + + while (true) { + + // Have we processed the requested number of rows? + if ((rows > 0) && (++processed > rows)) { + break; + } + + // Expose the current row in the specified request attribute + setRowIndex(++rowIndex); + if (!isRowAvailable()) { + break; // Scrolled past the last row + } + + // Perform phase-specific processing as required + // on the *children* of the UIColumn (facets have + // been done a single time with rowIndex=-1 already) + for (UIComponent kid : renderedColumns) { + if (kid.getChildCount() > 0) { + for (UIComponent grandkid : kid.getChildren()) { + if (!grandkid.isRendered()) { + continue; + } + resetClientIdCacheRecursively(grandkid); + if (phaseId == PhaseId.APPLY_REQUEST_VALUES) { + grandkid.processDecodes(context); + } else if (phaseId == PhaseId.PROCESS_VALIDATIONS) { + grandkid.processValidators(context); + } else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) { + grandkid.processUpdates(context); + } else { + throw new IllegalArgumentException(); + } + } + } + } + + } + + // Clean up after ourselves + setRowIndex(-1); + + } + + /** + *

+ * Return true if we need to keep the saved per-child state + * information. This will be the case if any of the following are true: + *

+ * + *
    + * + *
  • there are messages queued with severity ERROR or FATAL.
  • + * + *
  • this UIData instance is nested inside of another + * UIData instance
  • + * + *
+ * + * @param context + * {@link FacesContext} for the current request + */ + private boolean keepSaved(FacesContext context) { + + return (contextHasErrorMessages(context) || isNestedWithinIterator()); + + } + + private Boolean isNestedWithinIterator() { + if (isNested == null) { + UIComponent parent = this; + while (null != (parent = parent.getParent())) { + if (parent instanceof UIData || parent.getClass().getName().endsWith("UIRepeat")) { + isNested = Boolean.TRUE; + break; + } + } + if (isNested == null) { + isNested = Boolean.FALSE; + } + return isNested; + } else { + return isNested; + } + } + + private boolean contextHasErrorMessages(FacesContext context) { + + FacesMessage.Severity sev = context.getMaximumSeverity(); + return (sev != null && (FacesMessage.SEVERITY_ERROR.compareTo(sev) >= 0)); + + } + + private void resetClientIdCacheRecursively(UIComponent c) { + String id=c.getId(); + if (null != id) { + c.setId(id); // this strange operation clears the cache of the clientId + } + Iterator children = c.getFacetsAndChildren(); + if (children != null) { + while (children.hasNext()) { + UIComponent kid = children.next(); + resetClientIdCacheRecursively(kid); + } + } + } + + }