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    
015    package org.apache.tapestry5.corelib.components;
016    
017    import java.io.IOException;
018    import java.util.Collections;
019    import java.util.List;
020    
021    import org.apache.tapestry5.*;
022    import org.apache.tapestry5.annotations.Component;
023    import org.apache.tapestry5.annotations.Environmental;
024    import org.apache.tapestry5.annotations.Parameter;
025    import org.apache.tapestry5.annotations.Persist;
026    import org.apache.tapestry5.annotations.Property;
027    import org.apache.tapestry5.annotations.SupportsInformalParameters;
028    import org.apache.tapestry5.beaneditor.BeanModel;
029    import org.apache.tapestry5.beaneditor.PropertyModel;
030    import org.apache.tapestry5.corelib.data.GridPagerPosition;
031    import org.apache.tapestry5.grid.ColumnSort;
032    import org.apache.tapestry5.grid.GridDataSource;
033    import org.apache.tapestry5.grid.GridModel;
034    import org.apache.tapestry5.grid.GridSortModel;
035    import org.apache.tapestry5.grid.SortConstraint;
036    import org.apache.tapestry5.internal.TapestryInternalUtils;
037    import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
038    import org.apache.tapestry5.internal.bindings.AbstractBinding;
039    import org.apache.tapestry5.ioc.annotations.Inject;
040    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
041    import org.apache.tapestry5.services.BeanModelSource;
042    import org.apache.tapestry5.services.ClientBehaviorSupport;
043    import org.apache.tapestry5.services.ComponentDefaultProvider;
044    import org.apache.tapestry5.services.ComponentEventResultProcessor;
045    import org.apache.tapestry5.services.FormSupport;
046    import 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
073    public 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    }