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