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