001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup.html.list; 018 019import java.io.Serializable; 020import java.util.Collections; 021import java.util.Iterator; 022import java.util.List; 023 024import org.apache.wicket.Component; 025import org.apache.wicket.markup.html.link.Link; 026import org.apache.wicket.markup.repeater.AbstractRepeater; 027import org.apache.wicket.model.IModel; 028import org.apache.wicket.model.Model; 029import org.apache.wicket.util.collections.ReadOnlyIterator; 030 031 032/** 033 * A ListView is a repeater that makes it easy to display/work with {@link List}s. However, there 034 * are situations where it is necessary to work with other collection types, for repeaters that 035 * might work better with non-list or database-driven collections see the 036 * org.apache.wicket.markup.repeater package. 037 * 038 * Also notice that in a list the item's uniqueness/primary key/id is identified as its index in the 039 * list. If this is not the case you should either override {@link #getListItemModel(IModel, int)} 040 * to return a model that will work with the item's true primary key, or use a different repeater 041 * that does not rely on the list index. 042 * 043 * A ListView holds ListItem children. Items can be re-ordered and deleted, either one at a time or 044 * many at a time. 045 * <p> 046 * Example: 047 * 048 * <pre> 049 * <tbody> 050 * <tr wicket:id="rows" class="even"> 051 * <td><span wicket:id="id">Test ID</span></td> 052 * ... 053 * </pre> 054 * 055 * <p> 056 * Though this example is about a HTML table, ListView is not at all limited to HTML tables. Any 057 * kind of list can be rendered using ListView. 058 * <p> 059 * The related Java code: 060 * 061 * <pre> 062 * add(new ListView<UserDetails>("rows", listData) 063 * { 064 * public void populateItem(final ListItem<UserDetails> item) 065 * { 066 * final UserDetails user = item.getModelObject(); 067 * item.add(new Label("id", user.getId())); 068 * } 069 * }); 070 * </pre> 071 * 072 * <p> 073 * <strong>NOTE:</strong> 074 * 075 * When you want to change the default generated markup it is important to realize that the ListView 076 * instance itself does not correspond to any markup, however, the generated ListItems do.<br/> 077 * 078 * This means that methods like {@link #setRenderBodyOnly(boolean)} and 079 * {@link #add(org.apache.wicket.behavior.Behavior...)} should be invoked on the {@link ListItem} 080 * that is given in {@link #populateItem(ListItem)} method. 081 * </p> 082 * 083 * <p> 084 * <strong>WARNING:</strong> though you can nest ListViews within Forms, you HAVE to set the 085 * setReuseItems property to true in order to have validation work properly. By default, 086 * setReuseItems is false, which has the effect that ListView replaces all child components by new 087 * instances. The idea behind this is that you always render the fresh data, and as people usually 088 * use ListViews for displaying read-only lists (at least, that's what we think), this is good 089 * default behavior. <br /> 090 * However, as the components are replaced before the rendering starts, the search for specific 091 * messages for these components fails as they are replaced with other instances. Another problem is 092 * that 'wrong' user input is kept as (temporary) instance data of the components. As these 093 * components are replaced by new ones, your user will never see the wrong data when setReuseItems 094 * is false. 095 * </p> 096 * 097 * @author Jonathan Locke 098 * @author Juergen Donnerstag 099 * @author Johan Compagner 100 * @author Eelco Hillenius 101 * 102 * @param <T> 103 * type of elements contained in the model's list 104 */ 105public abstract class ListView<T> extends AbstractRepeater 106{ 107 private static final long serialVersionUID = 1L; 108 109 /** Index of the first item to show */ 110 private int firstIndex = 0; 111 112 /** 113 * If true, re-rendering the list view is more efficient if the window doesn't get changed at 114 * all or if it gets scrolled (compared to paging). But if you modify the listView model object, 115 * than you must manually call listView.removeAll() in order to rebuild the ListItems. If you 116 * nest a ListView in a Form, ALWAYS set this property to true, as otherwise validation will not 117 * work properly. 118 */ 119 private boolean reuseItems = false; 120 121 /** Max number (not index) of items to show */ 122 private int viewSize = Integer.MAX_VALUE; 123 124 /** 125 * @see org.apache.wicket.Component#Component(String) 126 */ 127 public ListView(final String id) 128 { 129 super(id); 130 } 131 132 /** 133 * @param id component id 134 * @param model model containing a list of 135 * @see org.apache.wicket.Component#Component(String, IModel) 136 */ 137 public ListView(final String id, final IModel<? extends List<T>> model) 138 { 139 super(id, model); 140 141 if (model == null) 142 { 143 throw new IllegalArgumentException( 144 "Null models are not allowed. If you have no model, you may prefer a Loop instead"); 145 } 146 147 // A reasonable default for viewSize can not be determined right now, 148 // because list items might be added or removed until ListView 149 // gets rendered. 150 } 151 152 /** 153 * @param id 154 * See Component 155 * @param list 156 * List to cast to Serializable 157 * @see org.apache.wicket.Component#Component(String, IModel) 158 */ 159 public ListView(final String id, final List<T> list) 160 { 161 this(id, Model.ofList(list)); 162 } 163 164 /** 165 * Gets the list of items in the listView. This method is final because it is not designed to be 166 * overridden. If it were allowed to be overridden, the values returned by getModelObject() and 167 * getList() might not coincide. 168 * 169 * @return The list of items in this list view. 170 */ 171 @SuppressWarnings("unchecked") 172 public final List<T> getList() 173 { 174 final List<T> list = (List<T>)getDefaultModelObject(); 175 if (list == null) 176 { 177 return Collections.emptyList(); 178 } 179 return list; 180 } 181 182 /** 183 * If true re-rendering the list view is more efficient if the windows doesn't get changed at 184 * all or if it gets scrolled (compared to paging). But if you modify the listView model object, 185 * then you must manually call listView.removeAll() in order to rebuild the ListItems. If you 186 * nest a ListView in a Form, ALLWAYS set this property to true, as otherwise validation will 187 * not work properly. 188 * 189 * @return Whether to reuse items 190 */ 191 public boolean getReuseItems() 192 { 193 return reuseItems; 194 } 195 196 /** 197 * Get index of first cell in page. Default is: 0. 198 * 199 * @return Index of first cell in page. Default is: 0 200 */ 201 public final int getStartIndex() 202 { 203 return firstIndex; 204 } 205 206 /** 207 * Based on the model object's list size, firstIndex and view size, determine what the view size 208 * really will be. E.g. default for viewSize is Integer.MAX_VALUE, if not set via setViewSize(). 209 * If the underlying list has 10 elements, the value returned by getViewSize() will be 10 if 210 * startIndex = 0. 211 * 212 * @return The number of items to be populated and rendered. 213 */ 214 public int getViewSize() 215 { 216 int size = viewSize; 217 218 final Object modelObject = getDefaultModelObject(); 219 if (modelObject == null) 220 { 221 return 0; 222 } 223 224 // Adjust view size to model object's list size 225 final int modelSize = getList().size(); 226 if (firstIndex > modelSize) 227 { 228 return 0; 229 } 230 231 if ((size == Integer.MAX_VALUE) || ((firstIndex + size) > modelSize)) 232 { 233 size = modelSize - firstIndex; 234 } 235 236 // firstIndex + size must be smaller than Integer.MAX_VALUE 237 if ((Integer.MAX_VALUE - size) < firstIndex) 238 { 239 throw new IllegalStateException( 240 "firstIndex + size must be smaller than Integer.MAX_VALUE"); 241 } 242 243 return size; 244 } 245 246 /** 247 * Returns a link that will move the given item "down" (towards the end) in the listView. 248 * 249 * @param id 250 * Name of move-down link component to create 251 * @param item 252 * @return The link component 253 */ 254 public final Link<Void> moveDownLink(final String id, final ListItem<T> item) 255 { 256 return new Link<Void>(id) 257 { 258 private static final long serialVersionUID = 1L; 259 260 /** 261 * @see org.apache.wicket.markup.html.link.Link#onClick() 262 */ 263 @Override 264 public void onClick() 265 { 266 final int index = item.getIndex(); 267 if (index != -1) 268 { 269 addStateChange(); 270 271 // Swap list items and invalidate listView 272 Collections.swap(getList(), index, index + 1); 273 ListView.this.removeAll(); 274 } 275 } 276 277 @Override 278 public boolean isEnabled() 279 { 280 return item.getIndex() != (getList().size() - 1); 281 } 282 283 }; 284 } 285 286 /** 287 * Returns a link that will move the given item "up" (towards the beginning) in the listView. 288 * 289 * @param id 290 * Name of move-up link component to create 291 * @param item 292 * @return The link component 293 */ 294 public final Link<Void> moveUpLink(final String id, final ListItem<T> item) 295 { 296 return new Link<Void>(id) 297 { 298 private static final long serialVersionUID = 1L; 299 300 /** 301 * @see org.apache.wicket.markup.html.link.Link#onClick() 302 */ 303 @Override 304 public void onClick() 305 { 306 final int index = item.getIndex(); 307 if (index != -1) 308 { 309 310 addStateChange(); 311 312 // Swap items and invalidate listView 313 Collections.swap(getList(), index, index - 1); 314 ListView.this.removeAll(); 315 } 316 } 317 318 @Override 319 public boolean isEnabled() 320 { 321 return item.getIndex() != 0; 322 } 323 }; 324 } 325 326 /** 327 * Returns a link that will remove this ListItem from the ListView that holds it. 328 * 329 * @param id 330 * Name of remove link component to create 331 * @param item 332 * @return The link component 333 */ 334 public final Link<Void> removeLink(final String id, final ListItem<T> item) 335 { 336 return new Link<Void>(id) 337 { 338 private static final long serialVersionUID = 1L; 339 340 /** 341 * @see org.apache.wicket.markup.html.link.Link#onClick() 342 */ 343 @Override 344 public void onClick() 345 { 346 addStateChange(); 347 348 item.modelChanging(); 349 350 // Remove item and invalidate listView 351 getList().remove(item.getIndex()); 352 353 ListView.this.modelChanged(); 354 ListView.this.removeAll(); 355 } 356 }; 357 } 358 359 /** 360 * Sets the model as the provided list and removes all children, so that the next render will be 361 * using the contents of the model. 362 * 363 * @param list 364 * The list for the new model. The list must implement {@link Serializable}. 365 * @return This for chaining 366 */ 367 public ListView<T> setList(List<T> list) 368 { 369 setDefaultModel(Model.ofList(list)); 370 return this; 371 } 372 373 /** 374 * If true re-rendering the list view is more efficient if the windows doesn't get changed at 375 * all or if it gets scrolled (compared to paging). But if you modify the listView model object, 376 * than you must manually call listView.removeAll() in order to rebuild the ListItems. If you 377 * nest a ListView in a Form, <strong>always</strong> set this property to true, 378 * as otherwise validation will not work properly. 379 * 380 * @param reuseItems 381 * Whether to reuse the child items. 382 * @return this 383 */ 384 public ListView<T> setReuseItems(boolean reuseItems) 385 { 386 this.reuseItems = reuseItems; 387 return this; 388 } 389 390 /** 391 * Set the index of the first item to render 392 * 393 * @param startIndex 394 * First index of model object's list to display 395 * @return This 396 */ 397 public ListView<T> setStartIndex(final int startIndex) 398 { 399 firstIndex = startIndex; 400 401 if (firstIndex < 0) 402 { 403 firstIndex = 0; 404 } 405 else if (firstIndex > getList().size()) 406 { 407 firstIndex = 0; 408 } 409 410 return this; 411 } 412 413 /** 414 * Define the maximum number of items to render. Default: render all. 415 * 416 * @param size 417 * Number of items to display 418 * @return This 419 */ 420 public ListView<T> setViewSize(final int size) 421 { 422 viewSize = size; 423 424 if (viewSize < 0) 425 { 426 viewSize = Integer.MAX_VALUE; 427 } 428 429 return this; 430 } 431 432 /** 433 * Subclasses may provide their own ListItemModel with extended functionality. The default 434 * ListItemModel works fine with mostly static lists where index remains valid. In cases where 435 * the underlying list changes a lot (many users using the application), it may not longer be 436 * appropriate. In that case your own ListItemModel implementation should use an id (e.g. the 437 * database' record id) to identify and load the list item model object. 438 * 439 * @param listViewModel 440 * The ListView's model 441 * @param index 442 * The list item index 443 * @return The ListItemModel created 444 */ 445 protected IModel<T> getListItemModel(final IModel<? extends List<T>> listViewModel, 446 final int index) 447 { 448 return new ListItemModel<>(this, index); 449 } 450 451 /** 452 * Create a new ListItem for list item at index. 453 * 454 * @param index 455 * @param itemModel 456 * object in the list that the item represents 457 * @return ListItem 458 */ 459 protected ListItem<T> newItem(final int index, IModel<T> itemModel) 460 { 461 return new ListItem<>(index, itemModel); 462 } 463 464 /** 465 * @see org.apache.wicket.markup.repeater.AbstractRepeater#onPopulate() 466 */ 467 @SuppressWarnings("unchecked") 468 @Override 469 protected final void onPopulate() 470 { 471 // Get number of items to be displayed 472 final int size = getViewSize(); 473 if (size > 0) 474 { 475 if (getReuseItems()) 476 { 477 // Remove all ListItems no longer required 478 final int maxIndex = firstIndex + size; 479 for (final Iterator<Component> iterator = iterator(); iterator.hasNext();) 480 { 481 // Get next child component 482 final ListItem<?> child = (ListItem<?>)iterator.next(); 483 if (child != null) 484 { 485 final int index = child.getIndex(); 486 if (index < firstIndex || index >= maxIndex) 487 { 488 iterator.remove(); 489 } 490 } 491 } 492 } 493 else 494 { 495 // Automatically rebuild all ListItems before rendering the 496 // list view 497 removeAll(); 498 } 499 500 boolean hasChildren = size() != 0; 501 // Loop through the markup in this container for each item 502 for (int i = 0; i < size; i++) 503 { 504 // Get index 505 final int index = firstIndex + i; 506 507 ListItem<T> item = null; 508 if (hasChildren) 509 { 510 // If this component does not already exist, populate it 511 item = (ListItem<T>)get(Integer.toString(index)); 512 } 513 if (item == null) 514 { 515 // Create item for index 516 item = newItem(index, getListItemModel(getModel(), index)); 517 518 // Add list item 519 add(item); 520 521 // Populate the list item 522 onBeginPopulateItem(item); 523 populateItem(item); 524 } 525 } 526 } 527 else 528 { 529 removeAll(); 530 } 531 532 } 533 534 /** 535 * Comes handy for ready made ListView based components which must implement populateItem() but 536 * you don't want to lose compile time error checking reminding the user to implement abstract 537 * populateItem(). 538 * 539 * @param item 540 */ 541 protected void onBeginPopulateItem(final ListItem<T> item) 542 { 543 } 544 545 /** 546 * Populate a given item. 547 * <p> 548 * <b>be careful</b> to add any components to the list item. So, don't do: 549 * 550 * <pre> 551 * add(new Label("foo", "bar")); 552 * </pre> 553 * 554 * but: 555 * 556 * <pre> 557 * item.add(new Label("foo", "bar")); 558 * </pre> 559 * 560 * </p> 561 * 562 * @param item 563 * The item to populate 564 */ 565 protected abstract void populateItem(final ListItem<T> item); 566 567 /** 568 * @see org.apache.wicket.markup.repeater.AbstractRepeater#renderChild(org.apache.wicket.Component) 569 */ 570 @Override 571 protected final void renderChild(Component child) 572 { 573 renderItem((ListItem<?>)child); 574 } 575 576 /** 577 * Render a single item. 578 * 579 * @param item 580 * The item to be rendered 581 */ 582 protected void renderItem(final ListItem<?> item) 583 { 584 item.render(); 585 } 586 587 /** 588 * @see org.apache.wicket.markup.repeater.AbstractRepeater#renderIterator() 589 */ 590 @Override 591 protected Iterator<Component> renderIterator() 592 { 593 final int size = size(); 594 return new ReadOnlyIterator<Component>() 595 { 596 private int index = 0; 597 598 @Override 599 public boolean hasNext() 600 { 601 return index < size; 602 } 603 604 @Override 605 public Component next() 606 { 607 final String id = Integer.toString(firstIndex + index); 608 index++; 609 return get(id); 610 } 611 }; 612 } 613 614 /** 615 * Gets model 616 * 617 * @return model 618 */ 619 @SuppressWarnings("unchecked") 620 public final IModel<? extends List<T>> getModel() 621 { 622 return (IModel<? extends List<T>>)getDefaultModel(); 623 } 624 625 /** 626 * Sets model 627 * 628 * @param model 629 */ 630 public final void setModel(IModel<? extends List<T>> model) 631 { 632 setDefaultModel(model); 633 } 634 635 /** 636 * Gets model object 637 * 638 * @return model object 639 */ 640 @SuppressWarnings("unchecked") 641 public final List<T> getModelObject() 642 { 643 return (List<T>)getDefaultModelObject(); 644 } 645 646 /** 647 * Sets model object 648 * 649 * @param object 650 */ 651 public final void setModelObject(List<T> object) 652 { 653 setDefaultModelObject(object); 654 } 655}