/*
* 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);
}
}