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            // Furthermore, because the sort column id is cached, an unknown value exception must be prevented in case of dynamically created bean models (e.g. usage of the 'include' parameter)
422            final BeanModel dataModel = getDataModel();
423
424            if ((paginationModel == null || paginationModel.getSortColumnId() == null) || !(dataModel.getPropertyNames().contains(TapestryInternalUtils.extractIdFromPropertyExpression(paginationModel.getSortColumnId()))))
425            {
426                return Collections.emptyList();
427            }
428
429            PropertyModel sortModel = dataModel.getById(paginationModel.getSortColumnId());
430
431            SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());
432
433            return Collections.singletonList(constraint);
434        }
435
436        public void clear()
437        {
438            paginationModel.setSortColumnId(null);
439            paginationModel.setSortAscending(null);
440        }
441    }
442
443    GridSortModel defaultSortModel()
444    {
445        return new DefaultGridSortModel();
446    }
447
448    /**
449     * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source
450     * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide
451     * a different mechanism. The returning binding is variant (not invariant).
452     *
453     * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.commons.Messages)
454     */
455    protected Binding defaultModel()
456    {
457
458        return new AbstractBinding()
459        {
460            public Object get()
461            {
462                // Get the default row type from the data source
463
464                GridDataSource gridDataSource = source;
465
466                Class rowType = gridDataSource.getRowType();
467
468                if (renderTableIfEmpty || rowType == null)
469                    throw new RuntimeException(
470                            String.format(
471                                    "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.",
472                                    gridDataSource));
473
474                // Properties do not have to be read/write
475
476                return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
477            }
478
479            /**
480             * Returns false. This may be overkill, but it basically exists because the model is
481             * inherently mutable and therefore may contain client-specific state and needs to be
482             * discarded at the end of the request. If the model were immutable, then we could leave
483             * invariant as true.
484             */
485            @Override
486            public boolean isInvariant()
487            {
488                return false;
489            }
490        };
491    }
492
493    static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>()
494    {
495        private static final long serialVersionUID = 8545187927995722789L;
496
497        public void execute(Grid component)
498        {
499            component.setupDataSource();
500        }
501
502        @Override
503        public String toString()
504        {
505            return "Grid.SetupDataSource";
506        }
507    };
508
509    Object setupRender()
510    {
511        if (formSupport != null)
512        {
513            formSupport.store(this, SETUP_DATA_SOURCE);
514        }
515
516        setupDataSource();
517
518        // If there's no rows, display the empty block placeholder.
519
520        return !renderTableIfEmpty && cachingSource.isEmpty() ? empty : null;
521    }
522
523    void cleanupRender()
524    {
525        // if an inPlace Grid is rendered inside a Loop, be sure to generate a new wrapper
526        // zone for each iteration (TAP5-2256)
527        zone = null;
528
529        // If grid is rendered inside a Loop. be sure to generate a new data model for
530        // each iteration (TAP5-2470)
531        dataModel = null;
532    }
533
534    public GridPaginationModel getDefaultPaginationModel()
535    {
536        if (defaultPaginationModel == null)
537        {
538            defaultPaginationModel = new GridPaginationModelImpl();
539        }
540
541        return defaultPaginationModel;
542    }
543
544    void setupDataSource()
545    {
546        // TAP5-34: We pass the source into the CachingDataSource now; previously
547        // we were accessing source directly, but during submit the value wasn't
548        // cached, and therefore access was very inefficient, and sorting was
549        // very inconsistent during the processing of the form submission.
550
551        int effectiveCurrentPage = getCurrentPage();
552
553        int numberOfRowsRequiredToShowCurrentPage = 1 + (effectiveCurrentPage - 1) * rowsPerPage;
554        int numberOfRowsRequiredToFillCurrentPage = effectiveCurrentPage * rowsPerPage;
555
556        cachingSource = new CachingDataSource(source);
557        if (pagerPosition != GridPagerPosition.NONE)
558        {
559            // We're going to render the pager, so we need to determine the total number of rows anyway.
560            // We do that eagerly here so we don't have to perform two count operations; the subsequent
561            // ones will return a cached result
562            cachingSource.getAvailableRows();
563        }
564        int availableRowsWithLimit = cachingSource.getAvailableRows(numberOfRowsRequiredToFillCurrentPage);
565
566        if (availableRowsWithLimit == 0)
567            return;
568
569        // This captures when the number of rows has decreased, typically due to deletions.
570
571        if (numberOfRowsRequiredToShowCurrentPage > availableRowsWithLimit)
572        {
573            int maxPage = ((availableRowsWithLimit - 1) / rowsPerPage) + 1;
574            effectiveCurrentPage = maxPage;
575        }
576
577        int startIndex = (effectiveCurrentPage - 1) * rowsPerPage;
578
579        int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRowsWithLimit - 1);
580
581        cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints());
582    }
583
584    Object beginRender(MarkupWriter writer)
585    {
586        // Skip rendering of component (template, body, etc.) when there's nothing to display.
587        // The empty placeholder will already have rendered.
588
589        if (cachingSource.isEmpty())
590            return !renderTableIfEmpty ? false : null;
591
592        if (inPlace && zone == null)
593        {
594            javaScriptSupport.require("t5/core/zone");
595
596            writer.element("div", "data-container-type", "zone");
597
598            didRenderZoneDiv = true;
599
600            // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the
601            // GridPager and etc.  That's no longer necessary, so zone will always be null or "^".  We don't
602            // even need any special ids to be allocated!
603            zone = "^";
604        }
605
606        return null;
607    }
608
609    void afterRender(MarkupWriter writer)
610    {
611        if (didRenderZoneDiv)
612        {
613            writer.end(); // div
614            didRenderZoneDiv = false;
615        }
616    }
617
618    public BeanModel getDataModel()
619    {
620        if (dataModel == null)
621        {
622            dataModel = model;
623
624            BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
625        }
626
627        return dataModel;
628    }
629
630    public int getNumberOfProperties()
631    {
632        return getDataModel().getPropertyNames().size();
633    }
634
635    public GridDataSource getDataSource()
636    {
637        return cachingSource;
638    }
639
640    public GridSortModel getSortModel()
641    {
642        return sortModel;
643    }
644
645    public Object getPagerTop()
646    {
647        return pagerPosition.isMatchTop() ? pager : null;
648    }
649
650    public Object getPagerBottom()
651    {
652        return pagerPosition.isMatchBottom() ? pager : null;
653    }
654
655    public int getCurrentPage()
656    {
657        Integer currentPage = paginationModel.getCurrentPage();
658
659        return currentPage == null ? 1 : currentPage;
660    }
661
662    public void setCurrentPage(int currentPage)
663    {
664        paginationModel.setCurrentPage(currentPage);
665    }
666
667    private boolean getSortAscending()
668    {
669        Boolean sortAscending = paginationModel.getSortAscending();
670
671        return sortAscending != null && sortAscending.booleanValue();
672    }
673
674    private void setSortAscending(boolean sortAscending)
675    {
676        paginationModel.setSortAscending(sortAscending);
677    }
678
679    public int getRowsPerPage()
680    {
681        return rowsPerPage;
682    }
683
684    public Object getRow()
685    {
686        return row;
687    }
688
689    public void setRow(Object row)
690    {
691        this.row = row;
692    }
693
694    /**
695     * Resets the Grid to inital settings; this sets the current page to one, and
696     * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
697     */
698    public void reset()
699    {
700        sortModel.clear();
701        setCurrentPage(1);
702    }
703
704    /**
705     * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
706     * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
707     * re-render themselves. Invokes
708     * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the
709     * Grid component) as the content provider for the update.
710     */
711    void onInPlaceUpdate() throws IOException
712    {
713        this.zone = "^";
714
715        componentEventResultProcessor.processResultValue(this);
716    }
717
718    public String getClientId()
719    {
720        return table.getClientId();
721    }
722}