org.apache.wicket.markup.html.list.ListView.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.markup.html.list.ListView.java

Source

/*
 * 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>
 * &lt;tbody&gt;
 *   &lt;tr wicket:id=&quot;rows&quot; class=&quot;even&quot;&gt;
 *     &lt;td&gt;&lt;span wicket:id=&quot;id&quot;&gt;Test ID&lt;/span&gt;&lt;/td&gt;
 *     ...
 * </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&lt;UserDetails&gt;(&quot;rows&quot;, listData)
 * {
 *    public void populateItem(final ListItem&lt;UserDetails&gt; item)
 *    {
 *       final UserDetails user = item.getModelObject();
 *       item.add(new Label(&quot;id&quot;, 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(&quot;foo&quot;, &quot;bar&quot;));
     * </pre>
     * 
     * but:
     * 
     * <pre>
     * item.add(new Label(&quot;foo&quot;, &quot;bar&quot;));
     * </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);
    }
}