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.beaneditor.BeanModel;
018import org.apache.tapestry5.beaneditor.PropertyModel;
019import org.apache.tapestry5.corelib.data.GridPagerPosition;
020import org.apache.tapestry5.grid.*;
021import org.apache.tapestry5.internal.TapestryInternalUtils;
022import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
023import org.apache.tapestry5.internal.bindings.AbstractBinding;
024import org.apache.tapestry5.ioc.annotations.Inject;
025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
026import org.apache.tapestry5.services.BeanModelSource;
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.beaneditor.BeanModel
054 * @see org.apache.tapestry5.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.beaneditor.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.beaneditor.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.beaneditor.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.ioc.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 property. 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        CachingDataSource(GridDataSource delegate)
299        {
300            this.delegate = delegate;
301        }
302
303        public int getAvailableRows()
304        {
305            if (!availableRowsCached)
306            {
307                availableRows = delegate.getAvailableRows();
308                availableRowsCached = true;
309            }
310
311            return availableRows;
312        }
313
314        public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints)
315        {
316            delegate.prepare(startIndex, endIndex, sortConstraints);
317        }
318
319        public Object getRowValue(int index)
320        {
321            return delegate.getRowValue(index);
322        }
323
324        public Class getRowType()
325        {
326            return delegate.getRowType();
327        }
328    }
329
330    /**
331     * Default implementation that only allows a single column to be the sort column, and stores the sort information as
332     * persistent fields of the Grid component.
333     */
334    class DefaultGridSortModel implements GridSortModel
335    {
336        public ColumnSort getColumnSort(String columnId)
337        {
338            if (paginationModel == null || !TapestryInternalUtils.isEqual(columnId, paginationModel.getSortColumnId()))
339            {
340                return ColumnSort.UNSORTED;
341            }
342
343            return getColumnSort();
344        }
345
346        private ColumnSort getColumnSort()
347        {
348            return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING;
349        }
350
351        public void updateSort(String columnId)
352        {
353            assert InternalUtils.isNonBlank(columnId);
354
355            if (columnId.equals(paginationModel.getSortColumnId()))
356            {
357                setSortAscending(!getSortAscending());
358                return;
359            }
360
361            paginationModel.setSortColumnId(columnId);
362            setSortAscending(true);
363        }
364
365        public List<SortConstraint> getSortConstraints()
366        {
367            // In a few limited cases we may not have yet hit the SetupRender phase, and the model may be null.
368            if (paginationModel == null || paginationModel.getSortColumnId() == null)
369            {
370                return Collections.emptyList();
371            }
372
373            PropertyModel sortModel = getDataModel().getById(paginationModel.getSortColumnId());
374
375            SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());
376
377            return Collections.singletonList(constraint);
378        }
379
380        public void clear()
381        {
382            paginationModel.setSortColumnId(null);
383            paginationModel.setSortAscending(null);
384        }
385    }
386
387    GridSortModel defaultSortModel()
388    {
389        return new DefaultGridSortModel();
390    }
391
392    /**
393     * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source
394     * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide
395     * a different mechanism. The returning binding is variant (not invariant).
396     *
397     * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages)
398     */
399    protected Binding defaultModel()
400    {
401
402        return new AbstractBinding()
403        {
404            public Object get()
405            {
406                // Get the default row type from the data source
407
408                GridDataSource gridDataSource = source;
409
410                Class rowType = gridDataSource.getRowType();
411
412                if (renderTableIfEmpty || rowType == null)
413                    throw new RuntimeException(
414                            String.format(
415                                    "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.",
416                                    gridDataSource));
417
418                // Properties do not have to be read/write
419
420                return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
421            }
422
423            /**
424             * Returns false. This may be overkill, but it basically exists because the model is
425             * inherently mutable and therefore may contain client-specific state and needs to be
426             * discarded at the end of the request. If the model were immutable, then we could leave
427             * invariant as true.
428             */
429            @Override
430            public boolean isInvariant()
431            {
432                return false;
433            }
434        };
435    }
436
437    static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>()
438    {
439        private static final long serialVersionUID = 8545187927995722789L;
440
441        public void execute(Grid component)
442        {
443            component.setupDataSource();
444        }
445
446        @Override
447        public String toString()
448        {
449            return "Grid.SetupDataSource";
450        }
451    };
452
453    Object setupRender()
454    {
455        if (formSupport != null)
456        {
457            formSupport.store(this, SETUP_DATA_SOURCE);
458        }
459
460        setupDataSource();
461
462        // If there's no rows, display the empty block placeholder.
463
464        return !renderTableIfEmpty && cachingSource.getAvailableRows() == 0 ? empty : null;
465    }
466
467    void cleanupRender()
468    {
469        // if an inPlace Grid is rendered inside a Loop, be sure to generate a new wrapper
470        // zone for each iteration (TAP5-2256)
471        zone = null;
472
473        // If grid is rendered inside a Loop. be sure to generate a new data model for
474        // each iteration (TAP5-2470)
475        dataModel = null;
476    }
477
478    public GridPaginationModel getDefaultPaginationModel()
479    {
480        if (defaultPaginationModel == null)
481        {
482            defaultPaginationModel = new GridPaginationModelImpl();
483        }
484
485        return defaultPaginationModel;
486    }
487
488    void setupDataSource()
489    {
490        // TAP5-34: We pass the source into the CachingDataSource now; previously
491        // we were accessing source directly, but during submit the value wasn't
492        // cached, and therefore access was very inefficient, and sorting was
493        // very inconsistent during the processing of the form submission.
494
495        cachingSource = new CachingDataSource(source);
496
497        int availableRows = cachingSource.getAvailableRows();
498
499        if (availableRows == 0)
500            return;
501
502        int maxPage = ((availableRows - 1) / rowsPerPage) + 1;
503
504        // This captures when the number of rows has decreased, typically due to deletions.
505
506        int effectiveCurrentPage = getCurrentPage();
507
508        if (effectiveCurrentPage > maxPage)
509            effectiveCurrentPage = maxPage;
510
511        int startIndex = (effectiveCurrentPage - 1) * rowsPerPage;
512
513        int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRows - 1);
514
515        cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints());
516    }
517
518    Object beginRender(MarkupWriter writer)
519    {
520        // Skip rendering of component (template, body, etc.) when there's nothing to display.
521        // The empty placeholder will already have rendered.
522
523        if (cachingSource.getAvailableRows() == 0)
524            return !renderTableIfEmpty ? false : null;
525
526        if (inPlace && zone == null)
527        {
528            javaScriptSupport.require("t5/core/zone");
529
530            writer.element("div", "data-container-type", "zone");
531
532            didRenderZoneDiv = true;
533
534            // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the
535            // GridPager and etc.  That's no longer necessary, so zone will always be null or "^".  We don't
536            // even need any special ids to be allocated!
537            zone = "^";
538        }
539
540        return null;
541    }
542
543    void afterRender(MarkupWriter writer)
544    {
545        if (didRenderZoneDiv)
546        {
547            writer.end(); // div
548            didRenderZoneDiv = false;
549        }
550    }
551
552    public BeanModel getDataModel()
553    {
554        if (dataModel == null)
555        {
556            dataModel = model;
557
558            BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
559        }
560
561        return dataModel;
562    }
563
564    public int getNumberOfProperties()
565    {
566        return getDataModel().getPropertyNames().size();
567    }
568
569    public GridDataSource getDataSource()
570    {
571        return cachingSource;
572    }
573
574    public GridSortModel getSortModel()
575    {
576        return sortModel;
577    }
578
579    public Object getPagerTop()
580    {
581        return pagerPosition.isMatchTop() ? pager : null;
582    }
583
584    public Object getPagerBottom()
585    {
586        return pagerPosition.isMatchBottom() ? pager : null;
587    }
588
589    public int getCurrentPage()
590    {
591        Integer currentPage = paginationModel.getCurrentPage();
592
593        return currentPage == null ? 1 : currentPage;
594    }
595
596    public void setCurrentPage(int currentPage)
597    {
598        paginationModel.setCurrentPage(currentPage);
599    }
600
601    private boolean getSortAscending()
602    {
603        Boolean sortAscending = paginationModel.getSortAscending();
604
605        return sortAscending != null && sortAscending.booleanValue();
606    }
607
608    private void setSortAscending(boolean sortAscending)
609    {
610        paginationModel.setSortAscending(sortAscending);
611    }
612
613    public int getRowsPerPage()
614    {
615        return rowsPerPage;
616    }
617
618    public Object getRow()
619    {
620        return row;
621    }
622
623    public void setRow(Object row)
624    {
625        this.row = row;
626    }
627
628    /**
629     * Resets the Grid to inital settings; this sets the current page to one, and
630     * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
631     */
632    public void reset()
633    {
634        sortModel.clear();
635        setCurrentPage(1);
636    }
637
638    /**
639     * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
640     * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
641     * re-render themselves. Invokes
642     * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the
643     * Grid component) as the content provider for the update.
644     */
645    void onInPlaceUpdate() throws IOException
646    {
647        this.zone = "^";
648
649        componentEventResultProcessor.processResultValue(this);
650    }
651
652    public String getClientId()
653    {
654        return table.getClientId();
655    }
656}