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