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 <table> 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 }