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 * &lt;tbody&gt;
050 *   &lt;tr wicket:id=&quot;rows&quot; class=&quot;even&quot;&gt;
051 *     &lt;td&gt;&lt;span wicket:id=&quot;id&quot;&gt;Test ID&lt;/span&gt;&lt;/td&gt;
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&lt;UserDetails&gt;(&quot;rows&quot;, listData)
063 * {
064 *      public void populateItem(final ListItem&lt;UserDetails&gt; item)
065 *      {
066 *              final UserDetails user = item.getModelObject();
067 *              item.add(new Label(&quot;id&quot;, 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(&quot;foo&quot;, &quot;bar&quot;));
552         * </pre>
553         * 
554         * but:
555         * 
556         * <pre>
557         * item.add(new Label(&quot;foo&quot;, &quot;bar&quot;));
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}