    001    // Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.corelib.components;
    016
    017    import java.io.IOException;
    018    import java.util.Collections;
    019    import java.util.List;
    020
    021    import org.apache.tapestry5.*;
    022    import org.apache.tapestry5.annotations.Component;
    023    import org.apache.tapestry5.annotations.Environmental;
    024    import org.apache.tapestry5.annotations.Parameter;
    025    import org.apache.tapestry5.annotations.Persist;
    026    import org.apache.tapestry5.annotations.Property;
    027    import org.apache.tapestry5.annotations.SupportsInformalParameters;
    028    import org.apache.tapestry5.beaneditor.BeanModel;
    029    import org.apache.tapestry5.beaneditor.PropertyModel;
    030    import org.apache.tapestry5.corelib.data.GridPagerPosition;
    031    import org.apache.tapestry5.grid.ColumnSort;
    032    import org.apache.tapestry5.grid.GridDataSource;
    033    import org.apache.tapestry5.grid.GridModel;
    034    import org.apache.tapestry5.grid.GridSortModel;
    035    import org.apache.tapestry5.grid.SortConstraint;
    036    import org.apache.tapestry5.internal.TapestryInternalUtils;
    037    import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
    038    import org.apache.tapestry5.internal.bindings.AbstractBinding;
    039    import org.apache.tapestry5.ioc.annotations.Inject;
    040    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    041    import org.apache.tapestry5.services.BeanModelSource;
    042    import org.apache.tapestry5.services.ClientBehaviorSupport;
    043    import org.apache.tapestry5.services.ComponentDefaultProvider;
    044    import org.apache.tapestry5.services.ComponentEventResultProcessor;
    045    import org.apache.tapestry5.services.FormSupport;
    046    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    047
    048    /**
    049     * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The
    050     * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need.
    051     * <p/>
    052     * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of
    053     * properties, the default output-only behavior can be changed to produce a complex form with individual control for
    054     * editing properties of each row. There is a big caveat here: if the order of rows provided by
    055     * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the
    056     * possibility that data will be applied to the wrong server-side objects.
    057     * <p/>
    058     * For this reason, when using Grid and Form together, you should generally
    059     * provide the Grid with a {@link org.apache.tapestry5.ValueEncoder} (via the
    060     * encoder parameter), or use an entity type for the "row" parameter for which
    061     * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry
    062     * to use a unique ID for each row that doesn't change when rows are reordered.
    063     *
    064     * @see org.apache.tapestry5.beaneditor.BeanModel
    065     * @see org.apache.tapestry5.services.BeanModelSource
    066     * @see org.apache.tapestry5.grid.GridDataSource
    067     * @tapestrydoc
    068     * @see BeanEditForm
    069     * @see BeanDisplay
    070     * @see Loop
    071     */
    072    @SupportsInformalParameters
    073    public class Grid implements GridModel
    074    {
    075        /**
    076         * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit
    077         * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper
    078         * around the underlying List.
    079         */
    080        @Parameter(required = true, autoconnect = true)
    081        private GridDataSource source;
    082
    083        /**
    084         * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source
    085         * provided to sub-components.
    086         */
    087        private GridDataSource cachingSource;
    088
    089        /**
    090         * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up
    091         * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result
    092         * set.
    093         */
    094        @Parameter(BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_ROWS_PER_PAGE)
    095        private int rowsPerPage;
    096
    097        /**
    098         * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom",
    099         * "both" or "none".
    100         */
    101        @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_PAGER_POSITION,
    102                defaultPrefix = BindingConstants.LITERAL)
    103        private GridPagerPosition pagerPosition;
    104
    105        /**
    106         * Used to store the current object being rendered (for the current row). This is used when parameter blocks are
    107         * provided to override the default cell renderer for a particular column ... the components within the block can
    108         * use the property bound to the row parameter to know what they should render.
    109         */
    110        @Parameter(principal = true)
    111        private Object row;
    112
    113        /**
    114         * Optional output parmeter used to identify the index of the column being rendered.
    115         */
    116        @Parameter
    117        private int columnIndex;
    118
    119        /**
    120         * The model used to identify the properties to be presented and the order of presentation. The model may be
    121         * omitted, in which case a default model is generated from the first object in the data source (this implies that
    122         * the objects provided by the source are uniform). The model may be explicitly specified to override the default
    123         * behavior, say to reorder or rename columns or add additional columns. The add, include,
    124         * exclude and reorder
    125         * parameters are <em>only</em> applied to a default model, not an explicitly provided one.
    126         */
    127        @Parameter
    128        private BeanModel model;
    129
    130        /**
    131         * The model parameter after modification due to the add, include, exclude and reorder parameters.
    132         */
    133        private BeanModel dataModel;
    134
    135        /**
    136         * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports
    137         * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is
    138         * stored as persistent fields of the Grid component.
    139         */
    140        @Parameter
    141        private GridSortModel sortModel;
    142
    143        /**
    144         * A comma-seperated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel}.
    145         * Cells for added columns will be blank unless a cell override is provided. This parameter is only used
    146         * when a default model is created automatically.
    147         */
    148        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    149        private String add;
    150
    151        /**
    152         * A comma-separated list of property names to be retained from the
    153         * {@link org.apache.tapestry5.beaneditor.BeanModel}.
    154         * Only these properties will be retained, and the properties will also be reordered. The names are
    155         * case-insensitive. This parameter is only used
    156         * when a default model is created automatically.
    157         */
    158        @SuppressWarnings("unused")
    159        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    160        private String include;
    161
    162        /**
    163         * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel}
    164         * .
    165         * The names are case-insensitive. This parameter is only used
    166         * when a default model is created automatically.
    167         */
    168        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    169        private String exclude;
    170
    171        /**
    172         * A comma-separated list of property names indicating the order in which the properties should be presented. The
    173         * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display
    174         * order. This parameter is only used
    175         * when a default model is created automatically.
    176         */
    177        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    178        private String reorder;
    179
    180        /**
    181         * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text
    182         * "There is no data to display". This parameter is used to customize that message, possibly including components to
    183         * allow the user to create new objects.
    184         */
    185        //@Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_EMPTY_BLOCK,
    186        @Parameter(value = "block:empty",
    187                defaultPrefix = BindingConstants.LITERAL)
    188        private Block empty;
    189
    190        /**
    191         * CSS class for the &lt;table&gt; element. In addition, informal parameters to the Grid are rendered in the table
    192         * element.
    193         */
    194        @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL,
    195                value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_TABLE_CSS_CLASS)
    196        @Property(write = false)
    197        private String tableClass;
    198
    199        /**
    200         * If true, then the Grid will be wrapped in an element that acts like a
    201         * {@link org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone,
    202         * 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         * The name of the psuedo-zone that encloses the Grid.
    210         */
    211        @Property(write = false)
    212        private String zone;
    213
    214        private boolean didRenderZoneDiv;
    215
    216        @Persist
    217        private Integer currentPage;
    218
    219        @Persist
    220        private String sortColumnId;
    221
    222        @Persist
    223        private Boolean sortAscending;
    224
    225        @Inject
    226        private ComponentResources resources;
    227
    228        @Inject
    229        private BeanModelSource modelSource;
    230
    231        @Environmental
    232        private ClientBehaviorSupport clientBehaviorSupport;
    233
    234        @Component(parameters =
    235        { "index=inherit:columnIndex", "lean=inherit:lean", "overrides=overrides", "zone=zone" })
    236        private GridColumns columns;
    237
    238        @Component(parameters =
    239        { "columnIndex=inherit:columnIndex", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "row=row",
    240                "overrides=overrides" }, publishParameters = "rowIndex,rowClass,volatile,encoder,lean")
    241        private GridRows rows;
    242
    243        @Component(parameters =
    244        { "source=dataSource", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "zone=zone" })
    245        private GridPager pager;
    246
    247        @Component(parameters = "to=pagerTop")
    248        private Delegate pagerTop;
    249
    250        @Component(parameters = "to=pagerBottom")
    251        private Delegate pagerBottom;
    252
    253        @Component(parameters = "class=tableClass", inheritInformalParameters = true)
    254        private Any table;
    255
    256        @Environmental(false)
    257        private FormSupport formSupport;
    258
    259        @Environmental
    260        private JavaScriptSupport jsSupport;
    261
    262        /**
    263         * Defines where block and label overrides are obtained from. By default, the Grid component provides block
    264         * overrides (from its block parameters).
    265         */
    266        @Parameter(value = "this", allowNull = false)
    267        @Property(write = false)
    268        private PropertyOverrides overrides;
    269
    270        /**
    271         * Set up via the traditional or Ajax component event request handler
    272         */
    273        @Environmental
    274        private ComponentEventResultProcessor componentEventResultProcessor;
    275
    276        @Inject
    277        private ComponentDefaultProvider defaultsProvider;
    278
    279        ValueEncoder defaultEncoder()
    280        {
    281            return defaultsProvider.defaultValueEncoder("row", resources);
    282        }
    283
    284        /**
    285         * A version of GridDataSource that caches the availableRows property. This addresses TAPESTRY-2245.
    286         */
    287        static class CachingDataSource implements GridDataSource
    288        {
    289            private final GridDataSource delegate;
    290
    291            private boolean availableRowsCached;
    292
    293            private int availableRows;
    294
    295            CachingDataSource(GridDataSource delegate)
    296            {
    297                this.delegate = delegate;
    298            }
    299
    300            public int getAvailableRows()
    301            {
    302                if (!availableRowsCached)
    303                {
    304                    availableRows = delegate.getAvailableRows();
    305                    availableRowsCached = true;
    306                }
    307
    308                return availableRows;
    309            }
    310
    311            public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints)
    312            {
    313                delegate.prepare(startIndex, endIndex, sortConstraints);
    314            }
    315
    316            public Object getRowValue(int index)
    317            {
    318                return delegate.getRowValue(index);
    319            }
    320
    321            public Class getRowType()
    322            {
    323                return delegate.getRowType();
    324            }
    325        }
    326
    327        /**
    328         * Default implementation that only allows a single column to be the sort column, and stores the sort information as
    329         * persistent fields of the Grid component.
    330         */
    331        class DefaultGridSortModel implements GridSortModel
    332        {
    333            public ColumnSort getColumnSort(String columnId)
    334            {
    335                if (!TapestryInternalUtils.isEqual(columnId, sortColumnId))
    336                    return ColumnSort.UNSORTED;
    337
    338                return getColumnSort();
    339            }
    340
    341            private ColumnSort getColumnSort()
    342            {
    343                return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING;
    344            }
    345
    346            public void updateSort(String columnId)
    347            {
    348                assert InternalUtils.isNonBlank(columnId);
    349                if (columnId.equals(sortColumnId))
    350                {
    351                    setSortAscending(!getSortAscending());
    352                    return;
    353                }
    354
    355                sortColumnId = columnId;
    356                setSortAscending(true);
    357            }
    358
    359            public List<SortConstraint> getSortConstraints()
    360            {
    361                if (sortColumnId == null)
    362                    return Collections.emptyList();
    363
    364                PropertyModel sortModel = getDataModel().getById(sortColumnId);
    365
    366                SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());
    367
    368                return Collections.singletonList(constraint);
    369            }
    370
    371            public void clear()
    372            {
    373                sortColumnId = null;
    374            }
    375        }
    376
    377        GridSortModel defaultSortModel()
    378        {
    379            return new DefaultGridSortModel();
    380        }
    381
    382        /**
    383         * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source
    384         * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide
    385         * a different mechanism. The returning binding is variant (not invariant).
    386         *
    387         * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages)
    388         */
    389        protected Binding defaultModel()
    390        {
    391            return new AbstractBinding()
    392            {
    393                public Object get()
    394                {
    395                    // Get the default row type from the data source
    396
    397                    GridDataSource gridDataSource = source;
    398
    399                    Class rowType = gridDataSource.getRowType();
    400
    401                    if (rowType == null)
    402                        throw new RuntimeException(
    403                                String.format(
    404                                        "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.",
    405                                        gridDataSource));
    406
    407                    // Properties do not have to be read/write
    408
    409                    return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
    410                }
    411
    412                /**
    413                 * Returns false. This may be overkill, but it basically exists because the model is
    414                 * inherently mutable and therefore may contain client-specific state and needs to be
    415                 * discarded at the end of the request. If the model were immutable, then we could leave
    416                 * invariant as true.
    417                 */
    418                @Override
    419                public boolean isInvariant()
    420                {
    421                    return false;
    422                }
    423            };
    424        }
    425
    426        static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>()
    427        {
    428            private static final long serialVersionUID = 8545187927995722789L;
    429
    430            public void execute(Grid component)
    431            {
    432                component.setupDataSource();
    433            }
    434
    435            @Override
    436            public String toString()
    437            {
    438                return "Grid.SetupDataSource";
    439            }
    440        };
    441
    442        Object setupRender()
    443        {
    444            if (formSupport != null)
    445                formSupport.store(this, SETUP_DATA_SOURCE);
    446
    447            setupDataSource();
    448
    449            // If there's no rows, display the empty block placeholder.
    450
    451            return cachingSource.getAvailableRows() == 0 ? empty : null;
    452        }
    453
    454        void setupDataSource()
    455        {
    456            // TAP5-34: We pass the source into the CachingDataSource now; previously
    457            // we were accessing source directly, but during submit the value wasn't
    458            // cached, and therefore access was very inefficient, and sorting was
    459            // very inconsistent during the processing of the form submission.
    460
    461            cachingSource = new CachingDataSource(source);
    462
    463            int availableRows = cachingSource.getAvailableRows();
    464
    465            if (availableRows == 0)
    466                return;
    467
    468            int maxPage = ((availableRows - 1) / rowsPerPage) + 1;
    469
    470            // This captures when the number of rows has decreased, typically due to deletions.
    471
    472            int effectiveCurrentPage = getCurrentPage();
    473
    474            if (effectiveCurrentPage > maxPage)
    475                effectiveCurrentPage = maxPage;
    476
    477            int startIndex = (effectiveCurrentPage - 1) * rowsPerPage;
    478
    479            int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRows - 1);
    480
    481            dataModel = null;
    482
    483            cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints());
    484        }
    485
    486        Object beginRender(MarkupWriter writer)
    487        {
    488            // Skip rendering of component (template, body, etc.) when there's nothing to display.
    489            // The empty placeholder will already have rendered.
    490
    491            if (cachingSource.getAvailableRows() == 0)
    492                return false;
    493
    494            if (inPlace && zone == null)
    495            {
    496                zone = jsSupport.allocateClientId(resources);
    497
    498                writer.element("div", "id", zone);
    499
    500                clientBehaviorSupport.addZone(zone, null, "show");
    501
    502                didRenderZoneDiv = true;
    503            }
    504
    505            return null;
    506        }
    507
    508        void afterRender(MarkupWriter writer)
    509        {
    510            if (didRenderZoneDiv)
    511            {
    512                writer.end(); // div
    513                didRenderZoneDiv = false;
    514            }
    515        }
    516
    517        public BeanModel getDataModel()
    518        {
    519            if (dataModel == null)
    520            {
    521                dataModel = model;
    522
    523                BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
    524            }
    525
    526            return dataModel;
    527        }
    528
    529        public GridDataSource getDataSource()
    530        {
    531            return cachingSource;
    532        }
    533
    534        public GridSortModel getSortModel()
    535        {
    536            return sortModel;
    537        }
    538
    539        public Object getPagerTop()
    540        {
    541            return pagerPosition.isMatchTop() ? pager : null;
    542        }
    543
    544        public Object getPagerBottom()
    545        {
    546            return pagerPosition.isMatchBottom() ? pager : null;
    547        }
    548
    549        public int getCurrentPage()
    550        {
    551            return currentPage == null ? 1 : currentPage;
    552        }
    553
    554        public void setCurrentPage(int currentPage)
    555        {
    556            this.currentPage = currentPage;
    557        }
    558
    559        private boolean getSortAscending()
    560        {
    561            return sortAscending != null && sortAscending.booleanValue();
    562        }
    563
    564        private void setSortAscending(boolean sortAscending)
    565        {
    566            this.sortAscending = sortAscending;
    567        }
    568
    569        public int getRowsPerPage()
    570        {
    571            return rowsPerPage;
    572        }
    573
    574        public Object getRow()
    575        {
    576            return row;
    577        }
    578
    579        public void setRow(Object row)
    580        {
    581            this.row = row;
    582        }
    583
    584        /**
    585         * Resets the Grid to inital settings; this sets the current page to one, and
    586         * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
    587         */
    588        public void reset()
    589        {
    590            setCurrentPage(1);
    591            sortModel.clear();
    592        }
    593
    594        /**
    595         * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
    596         * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
    597         * re-render themselves. Invokes
    598         * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the
    599         * Grid component) as the content provider for the update.
    600         */
    601        void onInPlaceUpdate(String zone) throws IOException
    602        {
    603            this.zone = zone;
    604
    605            componentEventResultProcessor.processResultValue(this);
    606        }
    607    }