/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.markup.html.list; import java.io.Serializable; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.apache.wicket.Component; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.repeater.AbstractRepeater; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.collections.ReadOnlyIterator; /** * A ListView is a repeater that makes it easy to display/work with {@link List}s. However, there * are situations where it is necessary to work with other collection types, for repeaters that * might work better with non-list or database-driven collections see the * org.apache.wicket.markup.repeater package. * * Also notice that in a list the item's uniqueness/primary key/id is identified as its index in the * list. If this is not the case you should either override {@link #getListItemModel(IModel, int)} * to return a model that will work with the item's true primary key, or use a different repeater * that does not rely on the list index. * * A ListView holds ListItem children. Items can be re-ordered and deleted, either one at a time or * many at a time. * <p> * Example: * * <pre> * <tbody> * <tr wicket:id="rows" class="even"> * <td><span wicket:id="id">Test ID</span></td> * ... * </pre> * * <p> * Though this example is about a HTML table, ListView is not at all limited to HTML tables. Any * kind of list can be rendered using ListView. * <p> * The related Java code: * * <pre> * add(new ListView<UserDetails>("rows", listData) * { * public void populateItem(final ListItem<UserDetails> item) * { * final UserDetails user = item.getModelObject(); * item.add(new Label("id", user.getId())); * } * }); * </pre> * * <p> * <strong>NOTE:</strong> * * When you want to change the default generated markup it is important to realize that the ListView * instance itself does not correspond to any markup, however, the generated ListItems do.<br/> * * This means that methods like {@link #setRenderBodyOnly(boolean)} and * {@link #add(org.apache.wicket.behavior.Behavior...)} should be invoked on the {@link ListItem} * that is given in {@link #populateItem(ListItem)} method. * </p> * * <p> * <strong>WARNING:</strong> though you can nest ListViews within Forms, you HAVE to set the * setReuseItems property to true in order to have validation work properly. By default, * setReuseItems is false, which has the effect that ListView replaces all child components by new * instances. The idea behind this is that you always render the fresh data, and as people usually * use ListViews for displaying read-only lists (at least, that's what we think), this is good * default behavior. <br /> * However, as the components are replaced before the rendering starts, the search for specific * messages for these components fails as they are replaced with other instances. Another problem is * that 'wrong' user input is kept as (temporary) instance data of the components. As these * components are replaced by new ones, your user will never see the wrong data when setReuseItems * is false. * </p> * * @author Jonathan Locke * @author Juergen Donnerstag * @author Johan Compagner * @author Eelco Hillenius * * @param <T> * type of elements contained in the model's list */ public abstract class ListView<T> extends AbstractRepeater { private static final long serialVersionUID = 1L; /** Index of the first item to show */ private int firstIndex = 0; /** * If true, re-rendering the list view is more efficient if the window doesn't get changed at * all or if it gets scrolled (compared to paging). But if you modify the listView model object, * than you must manually call listView.removeAll() in order to rebuild the ListItems. If you * nest a ListView in a Form, ALWAYS set this property to true, as otherwise validation will not * work properly. */ private boolean reuseItems = false; /** Max number (not index) of items to show */ private int viewSize = Integer.MAX_VALUE; /** * @see org.apache.wicket.Component#Component(String) */ public ListView(final String id) { super(id); } /** * @param id component id * @param model model containing a list of * @see org.apache.wicket.Component#Component(String, IModel) */ public ListView(final String id, final IModel<? extends List<T>> model) { super(id, model); if (model == null) { throw new IllegalArgumentException( "Null models are not allowed. If you have no model, you may prefer a Loop instead"); } // A reasonable default for viewSize can not be determined right now, // because list items might be added or removed until ListView // gets rendered. } /** * @param id * See Component * @param list * List to cast to Serializable * @see org.apache.wicket.Component#Component(String, IModel) */ public ListView(final String id, final List<T> list) { this(id, Model.ofList(list)); } /** * Gets the list of items in the listView. This method is final because it is not designed to be * overridden. If it were allowed to be overridden, the values returned by getModelObject() and * getList() might not coincide. * * @return The list of items in this list view. */ @SuppressWarnings("unchecked") public final List<T> getList() { final List<T> list = (List<T>)getDefaultModelObject(); if (list == null) { return Collections.emptyList(); } return list; } /** * If true re-rendering the list view is more efficient if the windows doesn't get changed at * all or if it gets scrolled (compared to paging). But if you modify the listView model object, * then you must manually call listView.removeAll() in order to rebuild the ListItems. If you * nest a ListView in a Form, ALLWAYS set this property to true, as otherwise validation will * not work properly. * * @return Whether to reuse items */ public boolean getReuseItems() { return reuseItems; } /** * Get index of first cell in page. Default is: 0. * * @return Index of first cell in page. Default is: 0 */ public final int getStartIndex() { return firstIndex; } /** * Based on the model object's list size, firstIndex and view size, determine what the view size * really will be. E.g. default for viewSize is Integer.MAX_VALUE, if not set via setViewSize(). * If the underlying list has 10 elements, the value returned by getViewSize() will be 10 if * startIndex = 0. * * @return The number of items to be populated and rendered. */ public int getViewSize() { int size = viewSize; final Object modelObject = getDefaultModelObject(); if (modelObject == null) { return 0; } // Adjust view size to model object's list size final int modelSize = getList().size(); if (firstIndex > modelSize) { return 0; } if ((size == Integer.MAX_VALUE) || ((firstIndex + size) > modelSize)) { size = modelSize - firstIndex; } // firstIndex + size must be smaller than Integer.MAX_VALUE if ((Integer.MAX_VALUE - size) < firstIndex) { throw new IllegalStateException( "firstIndex + size must be smaller than Integer.MAX_VALUE"); } return size; } /** * Returns a link that will move the given item "down" (towards the end) in the listView. * * @param id * Name of move-down link component to create * @param item * @return The link component */ public final Link<Void> moveDownLink(final String id, final ListItem<T> item) { return new Link<Void>(id) { private static final long serialVersionUID = 1L; /** * @see org.apache.wicket.markup.html.link.Link#onClick() */ @Override public void onClick() { final int index = item.getIndex(); if (index != -1) { addStateChange(); // Swap list items and invalidate listView Collections.swap(getList(), index, index + 1); ListView.this.removeAll(); } } @Override public boolean isEnabled() { return item.getIndex() != (getList().size() - 1); } }; } /** * Returns a link that will move the given item "up" (towards the beginning) in the listView. * * @param id * Name of move-up link component to create * @param item * @return The link component */ public final Link<Void> moveUpLink(final String id, final ListItem<T> item) { return new Link<Void>(id) { private static final long serialVersionUID = 1L; /** * @see org.apache.wicket.markup.html.link.Link#onClick() */ @Override public void onClick() { final int index = item.getIndex(); if (index != -1) { addStateChange(); // Swap items and invalidate listView Collections.swap(getList(), index, index - 1); ListView.this.removeAll(); } } @Override public boolean isEnabled() { return item.getIndex() != 0; } }; } /** * Returns a link that will remove this ListItem from the ListView that holds it. * * @param id * Name of remove link component to create * @param item * @return The link component */ public final Link<Void> removeLink(final String id, final ListItem<T> item) { return new Link<Void>(id) { private static final long serialVersionUID = 1L; /** * @see org.apache.wicket.markup.html.link.Link#onClick() */ @Override public void onClick() { addStateChange(); item.modelChanging(); // Remove item and invalidate listView getList().remove(item.getIndex()); ListView.this.modelChanged(); ListView.this.removeAll(); } }; } /** * Sets the model as the provided list and removes all children, so that the next render will be * using the contents of the model. * * @param list * The list for the new model. The list must implement {@link Serializable}. * @return This for chaining */ public ListView<T> setList(List<T> list) { setDefaultModel(Model.ofList(list)); return this; } /** * If true re-rendering the list view is more efficient if the windows doesn't get changed at * all or if it gets scrolled (compared to paging). But if you modify the listView model object, * than you must manually call listView.removeAll() in order to rebuild the ListItems. If you * nest a ListView in a Form, <strong>always</strong> set this property to true, * as otherwise validation will not work properly. * * @param reuseItems * Whether to reuse the child items. * @return this */ public ListView<T> setReuseItems(boolean reuseItems) { this.reuseItems = reuseItems; return this; } /** * Set the index of the first item to render * * @param startIndex * First index of model object's list to display * @return This */ public ListView<T> setStartIndex(final int startIndex) { firstIndex = startIndex; if (firstIndex < 0) { firstIndex = 0; } else if (firstIndex > getList().size()) { firstIndex = 0; } return this; } /** * Define the maximum number of items to render. Default: render all. * * @param size * Number of items to display * @return This */ public ListView<T> setViewSize(final int size) { viewSize = size; if (viewSize < 0) { viewSize = Integer.MAX_VALUE; } return this; } /** * Subclasses may provide their own ListItemModel with extended functionality. The default * ListItemModel works fine with mostly static lists where index remains valid. In cases where * the underlying list changes a lot (many users using the application), it may not longer be * appropriate. In that case your own ListItemModel implementation should use an id (e.g. the * database' record id) to identify and load the list item model object. * * @param listViewModel * The ListView's model * @param index * The list item index * @return The ListItemModel created */ protected IModel<T> getListItemModel(final IModel<? extends List<T>> listViewModel, final int index) { return new ListItemModel<>(this, index); } /** * Create a new ListItem for list item at index. * * @param index * @param itemModel * object in the list that the item represents * @return ListItem */ protected ListItem<T> newItem(final int index, IModel<T> itemModel) { return new ListItem<>(index, itemModel); } /** * @see org.apache.wicket.markup.repeater.AbstractRepeater#onPopulate() */ @SuppressWarnings("unchecked") @Override protected final void onPopulate() { // Get number of items to be displayed final int size = getViewSize(); if (size > 0) { if (getReuseItems()) { // Remove all ListItems no longer required final int maxIndex = firstIndex + size; for (final Iterator<Component> iterator = iterator(); iterator.hasNext();) { // Get next child component final ListItem<?> child = (ListItem<?>)iterator.next(); if (child != null) { final int index = child.getIndex(); if (index < firstIndex || index >= maxIndex) { iterator.remove(); } } } } else { // Automatically rebuild all ListItems before rendering the // list view removeAll(); } boolean hasChildren = size() != 0; // Loop through the markup in this container for each item for (int i = 0; i < size; i++) { // Get index final int index = firstIndex + i; ListItem<T> item = null; if (hasChildren) { // If this component does not already exist, populate it item = (ListItem<T>)get(Integer.toString(index)); } if (item == null) { // Create item for index item = newItem(index, getListItemModel(getModel(), index)); // Add list item add(item); // Populate the list item onBeginPopulateItem(item); populateItem(item); } } } else { removeAll(); } } /** * Comes handy for ready made ListView based components which must implement populateItem() but * you don't want to lose compile time error checking reminding the user to implement abstract * populateItem(). * * @param item */ protected void onBeginPopulateItem(final ListItem<T> item) { } /** * Populate a given item. * <p> * <b>be careful</b> to add any components to the list item. So, don't do: * * <pre> * add(new Label("foo", "bar")); * </pre> * * but: * * <pre> * item.add(new Label("foo", "bar")); * </pre> * * </p> * * @param item * The item to populate */ protected abstract void populateItem(final ListItem<T> item); /** * @see org.apache.wicket.markup.repeater.AbstractRepeater#renderChild(org.apache.wicket.Component) */ @Override protected final void renderChild(Component child) { renderItem((ListItem<?>)child); } /** * Render a single item. * * @param item * The item to be rendered */ protected void renderItem(final ListItem<?> item) { item.render(); } /** * @see org.apache.wicket.markup.repeater.AbstractRepeater#renderIterator() */ @Override protected Iterator<Component> renderIterator() { final int size = size(); return new ReadOnlyIterator<Component>() { private int index = 0; @Override public boolean hasNext() { return index < size; } @Override public Component next() { final String id = Integer.toString(firstIndex + index); index++; return get(id); } }; } /** * Gets model * * @return model */ @SuppressWarnings("unchecked") public final IModel<? extends List<T>> getModel() { return (IModel<? extends List<T>>)getDefaultModel(); } /** * Sets model * * @param model */ public final void setModel(IModel<? extends List<T>> model) { setDefaultModel(model); } /** * Gets model object * * @return model object */ @SuppressWarnings("unchecked") public final List<T> getModelObject() { return (List<T>)getDefaultModelObject(); } /** * Sets model object * * @param object */ public final void setModelObject(List<T> object) { setDefaultModelObject(object); } }