Home » tapestry-src-5.0.19 » org.apache.tapestry5.corelib.components » [javadoc | source]

    1   // Copyright 2007, 2008 The Apache Software Foundation
    2   //
    3   // Licensed under the Apache License, Version 2.0 (the "License");
    4   // you may not use this file except in compliance with the License.
    5   // You may obtain a copy of the License at
    6   //
    7   //     http://www.apache.org/licenses/LICENSE-2.0
    8   //
    9   // Unless required by applicable law or agreed to in writing, software
   10   // distributed under the License is distributed on an "AS IS" BASIS,
   11   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   12   // See the License for the specific language governing permissions and
   13   // limitations under the License.
   14   
   15   package org.apache.tapestry5.corelib.components;
   16   
   17   import org.apache.tapestry5;
   18   import org.apache.tapestry5.annotations;
   19   import org.apache.tapestry5.beaneditor.BeanModel;
   20   import org.apache.tapestry5.beaneditor.PropertyModel;
   21   import org.apache.tapestry5.corelib.data.GridPagerPosition;
   22   import org.apache.tapestry5.grid;
   23   import org.apache.tapestry5.internal.TapestryInternalUtils;
   24   import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
   25   import org.apache.tapestry5.internal.bindings.AbstractBinding;
   26   import org.apache.tapestry5.internal.services.ClientBehaviorSupport;
   27   import org.apache.tapestry5.ioc.annotations.Inject;
   28   import org.apache.tapestry5.ioc.internal.util.Defense;
   29   import org.apache.tapestry5.services.BeanModelSource;
   30   import org.apache.tapestry5.services.ComponentEventResultProcessor;
   31   import org.apache.tapestry5.services.FormSupport;
   32   import org.apache.tapestry5.services.Request;
   33   
   34   import java.io.IOException;
   35   import java.util.Collections;
   36   import java.util.List;
   37   
   38   /**
   39    * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The
   40    * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need.
   41    * <p/>
   42    * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of
   43    * properties, the default output-only behavior can be changed to produce a complex form with individual control for
   44    * editing properties of each row. This is currently workable but less than ideal -- if the order of rows provided by
   45    * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the
   46    * possibility that data will be applied to the wrong server-side objects. In general, when using Grid and Form
   47    * together, you want to provide the Grid with a {@link org.apache.tapestry5.PrimaryKeyEncoder} (via the encoder
   48    * parameter).
   49    *
   50    * @see org.apache.tapestry5.beaneditor.BeanModel
   51    * @see org.apache.tapestry5.services.BeanModelSource
   52    * @see org.apache.tapestry5.grid.GridDataSource
   53    */
   54   @SupportsInformalParameters
   55   public class Grid implements GridModel
   56   {
   57       /**
   58        * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit
   59        * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper
   60        * around the underlying List.
   61        */
   62       @Parameter(required = true, autoconnect = true)
   63       private GridDataSource source;
   64   
   65       /**
   66        * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source
   67        * provided to sub-components.
   68        */
   69       private GridDataSource cachingSource;
   70   
   71       /**
   72        * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up
   73        * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result
   74        * set.
   75        */
   76       @Parameter("25")
   77       private int rowsPerPage;
   78   
   79       /**
   80        * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom",
   81        * "both" or "none".
   82        */
   83       @Parameter(value = "top", defaultPrefix = BindingConstants.LITERAL)
   84       private GridPagerPosition pagerPosition;
   85   
   86       /**
   87        * Used to store the current object being rendered (for the current row). This is used when parameter blocks are
   88        * provided to override the default cell renderer for a particular column ... the components within the block can
   89        * use the property bound to the row parameter to know what they should render.
   90        */
   91       @Parameter
   92       private Object row;
   93   
   94       /**
   95        * Optional output parameter used to identify the index (from zero) of the row being rendered.
   96        */
   97       @Parameter
   98       private int rowIndex;
   99   
  100       /**
  101        * Optional output parmater used to identify the index of the column being rendered.
  102        */
  103       @Parameter
  104       private int columnIndex;
  105   
  106       /**
  107        * The model used to identify the properties to be presented and the order of presentation. The model may be
  108        * omitted, in which case a default model is generated from the first object in the data source (this implies that
  109        * the objects provided by the source are uniform). The model may be explicitly specified to override the default
  110        * behavior, say to reorder or rename columns or add additional columns.
  111        */
  112       @Parameter
  113       private BeanModel model;
  114   
  115       /**
  116        * The model parameter after modification due to the add, include, exclude and reorder parameters.
  117        */
  118       private BeanModel dataModel;
  119   
  120       /**
  121        * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports
  122        * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is
  123        * stored as persistent fields of the Grid component.
  124        */
  125       @Parameter
  126       private GridSortModel sortModel;
  127   
  128       /**
  129        * A comma-seperated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel}.
  130        * Cells for added columns will be blank unless a cell override is provided.
  131        */
  132       @Parameter(defaultPrefix = BindingConstants.LITERAL)
  133       private String add;
  134   
  135       /**
  136        * A comma-separated list of property names to be retained from the {@link org.apache.tapestry5.beaneditor.BeanModel}.
  137        * Only these properties will be retained, and the properties will also be reordered. The names are
  138        * case-insensitive.
  139        */
  140       @SuppressWarnings("unused")
  141       @Parameter(defaultPrefix = BindingConstants.LITERAL)
  142       private String include;
  143   
  144       /**
  145        * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel}.
  146        * The names are case-insensitive.
  147        */
  148       @Parameter(defaultPrefix = BindingConstants.LITERAL)
  149       private String exclude;
  150   
  151       /**
  152        * A comma-separated list of property names indicating the order in which the properties should be presented. The
  153        * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display
  154        * order.
  155        */
  156       @Parameter(defaultPrefix = BindingConstants.LITERAL)
  157       private String reorder;
  158   
  159       /**
  160        * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text
  161        * "There is no data to display". This parameter is used to customize that message, possibly including components to
  162        * allow the user to create new objects.
  163        */
  164       @Parameter(value = "block:empty", defaultPrefix = BindingConstants.LITERAL)
  165       private Block empty;
  166   
  167   
  168       /**
  169        * If true, then the CSS class on each &lt;TD&gt; and &lt;TH&gt; cell will be omitted, which can reduce the amount
  170        * of output from the component overall by a considerable amount. Leave this as false, the default, when you are
  171        * leveraging the CSS to customize the look and feel of particular columns.
  172        */
  173       @Parameter
  174       private boolean lean;
  175   
  176       /**
  177        * If true and the Grid is enclosed by a Form, then the normal state persisting logic is turned off. Defaults to
  178        * false, enabling state persisting within Forms. If a Grid is present for some reason within a Form, but does not
  179        * contain any form control components (such as {@link TextField}), then binding volatile to false will reduce the
  180        * amount of client-side state that must be persisted.
  181        */
  182       @Parameter(name = "volatile")
  183       private boolean volatileState;
  184   
  185       /**
  186        * The CSS class for the tr element for each data row. This can be used to highlight particular rows, or cycle
  187        * between CSS values (for the "zebra effect"). If null or not bound, then no particular CSS class value is used.
  188        */
  189       @Parameter(cache = false)
  190       private String rowClass;
  191   
  192       /**
  193        * CSS class for the &lt;table&gt; element.  In addition, informal parameters to the Grid are rendered in the table
  194        * element.
  195        */
  196       @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL, value = "t-data-grid")
  197       @Property(write = false)
  198       private String tableClass;
  199   
  200       /**
  201        * If true, then the Grid will be wrapped in an element that acts like a {@link
  202        * org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone, repainting
  203        * the entire grid within it, but leaving the rest of the page (outside the zone) unchanged.
  204        */
  205       @Parameter
  206       private boolean inPlace;
  207   
  208       /**
  209        * Changes how state is recorded into the form to store the {@linkplain org.apache.tapestry5.PrimaryKeyEncoder#toKey(Object)
  210        * primary key} for each row (rather than the index), and restore the {@linkplain
  211        * org.apache.tapestry5.PrimaryKeyEncoder#toValue(java.io.Serializable) row values} from the primary keys.
  212        */
  213       @Parameter
  214       private PrimaryKeyEncoder encoder;
  215   
  216       /**
  217        * The name of the psuedo-zone that encloses the Grid.
  218        */
  219       @Property(write = false)
  220       private String zone;
  221   
  222       private boolean didRenderZoneDiv;
  223   
  224       @Persist
  225       private Integer currentPage;
  226   
  227       @Persist
  228       private String sortColumnId;
  229   
  230       @Persist
  231       private Boolean sortAscending;
  232   
  233   
  234       @Inject
  235       private ComponentResources resources;
  236   
  237       @Inject
  238       private BeanModelSource modelSource;
  239   
  240       @Environmental
  241       private ClientBehaviorSupport clientBehaviorSupport;
  242   
  243       @Component(
  244               parameters = {
  245                       "index=inherit:columnIndex",
  246                       "lean=inherit:lean",
  247                       "overrides=overrides",
  248                       "zone=zone"})
  249       private GridColumns columns;
  250   
  251       @Component(
  252               parameters = {
  253                       "rowIndex=inherit:rowIndex",
  254                       "columnIndex=inherit:columnIndex",
  255                       "rowClass=inherit:rowClass",
  256                       "rowsPerPage=rowsPerPage",
  257                       "currentPage=currentPage",
  258                       "row=row",
  259                       "overrides=overrides",
  260                       "volatile=inherit:volatile",
  261                       "encoder=inherit:encoder",
  262                       "lean=inherit:lean"})
  263       private GridRows rows;
  264   
  265       @Component(parameters = {
  266               "source=dataSource",
  267               "rowsPerPage=rowsPerPage",
  268               "currentPage=currentPage",
  269               "zone=zone"})
  270       private GridPager pager;
  271   
  272       @Component(parameters = "to=pagerTop")
  273       private Delegate pagerTop;
  274   
  275       @Component(parameters = "to=pagerBottom")
  276       private Delegate pagerBottom;
  277   
  278       @Component(parameters = "class=tableClass", inheritInformalParameters = true)
  279       private Any table;
  280   
  281       @Environmental(false)
  282       private FormSupport formSupport;
  283   
  284       @Inject
  285       private Request request;
  286   
  287       @Environmental
  288       private RenderSupport renderSupport;
  289   
  290       /**
  291        * Defines where block and label overrides are obtained from. By default, the Grid component provides block
  292        * overrides (from its block parameters).
  293        */
  294       @Parameter(value = "this", allowNull = false)
  295       @Property(write = false)
  296       private PropertyOverrides overrides;
  297   
  298       /**
  299        * Set up via the traditional or Ajax component event request handler
  300        */
  301       @Environmental
  302       private ComponentEventResultProcessor componentEventResultProcessor;
  303   
  304       /**
  305        * A version of GridDataSource that caches the availableRows property. This addresses TAPESTRY-2245.
  306        */
  307       static class CachingDataSource implements GridDataSource
  308       {
  309           private final GridDataSource delegate;
  310   
  311           private boolean availableRowsCached;
  312   
  313           private int availableRows;
  314   
  315           CachingDataSource(GridDataSource delegate)
  316           {
  317               this.delegate = delegate;
  318           }
  319   
  320           public int getAvailableRows()
  321           {
  322               if (!availableRowsCached)
  323               {
  324                   availableRows = delegate.getAvailableRows();
  325                   availableRowsCached = true;
  326               }
  327   
  328               return availableRows;
  329           }
  330   
  331           public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints)
  332           {
  333               delegate.prepare(startIndex, endIndex, sortConstraints);
  334           }
  335   
  336           public Object getRowValue(int index)
  337           {
  338               return delegate.getRowValue(index);
  339           }
  340   
  341           public Class getRowType()
  342           {
  343               return delegate.getRowType();
  344           }
  345       }
  346   
  347       /**
  348        * Default implementation that only allows a single column to be the sort column, and stores the sort information as
  349        * persistent fields of the Grid component.
  350        */
  351       class DefaultGridSortModel implements GridSortModel
  352       {
  353           public ColumnSort getColumnSort(String columnId)
  354           {
  355               if (!TapestryInternalUtils.isEqual(columnId, sortColumnId))
  356                   return ColumnSort.UNSORTED;
  357   
  358               return getColumnSort();
  359           }
  360   
  361           private ColumnSort getColumnSort()
  362           {
  363               return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING;
  364           }
  365   
  366   
  367           public void updateSort(String columnId)
  368           {
  369               Defense.notBlank(columnId, "columnId");
  370   
  371               if (columnId.equals(sortColumnId))
  372               {
  373                   setSortAscending(!getSortAscending());
  374                   return;
  375               }
  376   
  377               sortColumnId = columnId;
  378               setSortAscending(true);
  379           }
  380   
  381           public List<SortConstraint> getSortConstraints()
  382           {
  383               if (sortColumnId == null)
  384                   return Collections.emptyList();
  385   
  386               PropertyModel sortModel = getDataModel().getById(sortColumnId);
  387   
  388               SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());
  389   
  390               return Collections.singletonList(constraint);
  391           }
  392   
  393           public void clear()
  394           {
  395               sortColumnId = null;
  396           }
  397       }
  398   
  399       GridSortModel defaultSortModel()
  400       {
  401           return new DefaultGridSortModel();
  402       }
  403   
  404       /**
  405        * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source
  406        * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide
  407        * a different mechanism.  The returning binding is variant (not invariant).
  408        *
  409        * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages)
  410        */
  411       protected Binding defaultModel()
  412       {
  413           return new AbstractBinding()
  414           {
  415               public Object get()
  416               {
  417                   // Get the default row type from the data source
  418   
  419                   GridDataSource gridDataSource = source;
  420   
  421                   Class rowType = gridDataSource.getRowType();
  422   
  423                   if (rowType == null)
  424                       throw new RuntimeException(
  425                               String.format(
  426                                       "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.",
  427                                       gridDataSource));
  428   
  429                   // Properties do not have to be read/write
  430   
  431                   return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
  432               }
  433   
  434               /**
  435                * Returns false. This may be overkill, but it basically exists because the model is
  436                * inherently mutable and therefore may contain client-specific state and needs to be
  437                * discarded at the end of the request. If the model were immutable, then we could leave
  438                * invariant as true.
  439                */
  440               @Override
  441               public boolean isInvariant()
  442               {
  443                   return false;
  444               }
  445           };
  446       }
  447   
  448       static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>()
  449       {
  450           private static final long serialVersionUID = 8545187927995722789L;
  451   
  452           public void execute(Grid component)
  453           {
  454               component.setupDataSource();
  455           }
  456   
  457           @Override
  458           public String toString()
  459           {
  460               return "Grid.SetupDataSource";
  461           }
  462       };
  463   
  464       Object setupRender()
  465       {
  466           if (formSupport != null) formSupport.store(this, SETUP_DATA_SOURCE);
  467   
  468           setupDataSource();
  469   
  470           // If there's no rows, display the empty block placeholder.
  471   
  472           return cachingSource.getAvailableRows() == 0 ? empty : null;
  473       }
  474   
  475       void setupDataSource()
  476       {
  477           // TAP5-34: We pass the source into the CachingDataSource now; previously
  478           // we were accessing source directly, but during submit the value wasn't
  479           // cached, and therefore access was very inefficient, and sorting was
  480           // very inconsistent during the processing of the form submission.
  481   
  482           cachingSource = new CachingDataSource(source);
  483   
  484           int availableRows = cachingSource.getAvailableRows();
  485   
  486           if (availableRows == 0) return;
  487   
  488           int maxPage = ((availableRows - 1) / rowsPerPage) + 1;
  489   
  490           // This captures when the number of rows has decreased, typically due to deletions.
  491   
  492           int effectiveCurrentPage = getCurrentPage();
  493   
  494           if (effectiveCurrentPage > maxPage)
  495               effectiveCurrentPage = maxPage;
  496   
  497           int startIndex = (effectiveCurrentPage - 1) * rowsPerPage;
  498   
  499           int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRows - 1);
  500   
  501           dataModel = null;
  502   
  503           cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints());
  504       }
  505   
  506       Object beginRender(MarkupWriter writer)
  507       {
  508           // Skip rendering of component (template, body, etc.) when there's nothing to display.
  509           // The empty placeholder will already have rendered.
  510   
  511           if (cachingSource.getAvailableRows() == 0) return false;
  512   
  513           if (inPlace && zone == null)
  514           {
  515               zone = renderSupport.allocateClientId(resources);
  516   
  517               writer.element("div", "id", zone);
  518   
  519               clientBehaviorSupport.addZone(zone, null, "show");
  520   
  521               didRenderZoneDiv = true;
  522           }
  523   
  524           return null;
  525       }
  526   
  527       void afterRender(MarkupWriter writer)
  528       {
  529           if (didRenderZoneDiv)
  530           {
  531               writer.end(); // div
  532               didRenderZoneDiv = false;
  533           }
  534       }
  535   
  536       public BeanModel getDataModel()
  537       {
  538           if (dataModel == null)
  539           {
  540               dataModel = model;
  541   
  542               BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
  543           }
  544   
  545           return dataModel;
  546       }
  547   
  548       public GridDataSource getDataSource()
  549       {
  550           return cachingSource;
  551       }
  552   
  553       public GridSortModel getSortModel()
  554       {
  555           return sortModel;
  556       }
  557   
  558       public Object getPagerTop()
  559       {
  560           return pagerPosition.isMatchTop() ? pager : null;
  561       }
  562   
  563       public Object getPagerBottom()
  564       {
  565           return pagerPosition.isMatchBottom() ? pager : null;
  566       }
  567   
  568       public int getCurrentPage()
  569       {
  570           return currentPage == null ? 1 : currentPage;
  571       }
  572   
  573       public void setCurrentPage(int currentPage)
  574       {
  575           this.currentPage = currentPage;
  576       }
  577   
  578       private boolean getSortAscending()
  579       {
  580           return sortAscending != null && sortAscending.booleanValue();
  581       }
  582   
  583       private void setSortAscending(boolean sortAscending)
  584       {
  585           this.sortAscending = sortAscending;
  586       }
  587   
  588       public int getRowsPerPage()
  589       {
  590           return rowsPerPage;
  591       }
  592   
  593       public Object getRow()
  594       {
  595           return row;
  596       }
  597   
  598       public void setRow(Object row)
  599       {
  600           this.row = row;
  601       }
  602   
  603       /**
  604        * Resets the Grid to inital settings; this sets the current page to one, and {@linkplain
  605        * org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
  606        */
  607       public void reset()
  608       {
  609           setCurrentPage(1);
  610           sortModel.clear();
  611       }
  612   
  613       /**
  614        * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
  615        * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
  616        * re-render themselves.  Invokes {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)}
  617        * passing this (the Grid component) as the content provider for the update.
  618        */
  619       void onInPlaceUpdate(String zone) throws IOException
  620       {
  621           this.zone = zone;
  622   
  623           componentEventResultProcessor.processResultValue(this);
  624       }
  625   }

Home » tapestry-src-5.0.19 » org.apache.tapestry5.corelib.components » [javadoc | source]