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.repeater;
018
019import java.util.Collection;
020import java.util.Iterator;
021import java.util.NoSuchElementException;
022
023import org.apache.wicket.markup.html.navigation.paging.IPageableItems;
024import org.apache.wicket.model.IModel;
025
026
027/**
028 * An abstract repeater view that provides paging functionality to its subclasses.
029 * <p>
030 * The view is populated by overriding the <code>getItemModels(int offset, int count)</code> method
031 * and providing an iterator that returns models for items in the current page. The
032 * AbstractPageableView builds the items that will be rendered by looping over the models and
033 * calling the <code>newItem(String id, int index, IModel model)</code> to generate the child item
034 * container followed by <code>populateItem(Component item)</code> to let the user populate the
035 * newly created item container with with custom components.
036 * </p>
037 * 
038 * @see org.apache.wicket.markup.repeater.RefreshingView
039 * @see org.apache.wicket.markup.html.navigation.paging.IPageable
040 * 
041 * @author Igor Vaynberg (ivaynberg)
042 * 
043 * @param <T>
044 *            type of elements contained in the model's list
045 */
046public abstract class AbstractPageableView<T> extends RefreshingView<T> implements IPageableItems
047{
048        /** */
049        private static final long serialVersionUID = 1L;
050
051        /**
052         * Keeps track of the number of items we show per page. The default is Integer.MAX_VALUE which
053         * effectively disables paging.
054         */
055        private long itemsPerPage = Long.MAX_VALUE;
056
057        /**
058         * Keeps track of the current page number.
059         */
060        private long currentPage;
061
062        /**
063         * <code>cachedItemCount</code> is used to cache the call to <code>internalGetItemCount()</code>
064         * for the duration of the request because that call can potentially be expensive ( a select
065         * count query ) and so we do not want to execute it multiple times.
066         */
067        private transient Long cachedItemCount;
068
069        /**
070         * Constructor
071         * 
072         * @param id
073         * @param model
074         * @see org.apache.wicket.Component#Component(String, IModel)
075         */
076        public AbstractPageableView(String id, IModel<? extends Collection<? extends T>> model)
077        {
078                super(id, model);
079        }
080
081        /** @see org.apache.wicket.Component#Component(String) */
082        public AbstractPageableView(String id)
083        {
084                super(id);
085        }
086
087        /**
088         * This method retrieves the subset of models for items in the current page and allows
089         * RefreshingView to generate items.
090         * 
091         * @return iterator over models for items in the current page
092         */
093        @Override
094        protected Iterator<IModel<T>> getItemModels()
095        {
096                long offset = getFirstItemOffset();
097                long size = getViewSize();
098
099                Iterator<IModel<T>> models = getItemModels(offset, size);
100
101                models = new CappedIteratorAdapter<T>(models, size);
102
103                return models;
104        }
105
106        /**
107         * @see org.apache.wicket.markup.repeater.AbstractRepeater#onBeforeRender()
108         */
109        @Override
110        protected void onBeforeRender()
111        {
112                clearCachedItemCount();
113                super.onBeforeRender();
114        }
115
116        /**
117         * Returns an iterator over models for items in the current page
118         * 
119         * @param offset
120         *            index of first item in this page
121         * @param size
122         *            number of items that will be shown in the current page
123         * @return an iterator over models for items in the current page
124         */
125        protected abstract Iterator<IModel<T>> getItemModels(long offset, long size);
126
127        // /////////////////////////////////////////////////////////////////////////
128        // ITEM COUNT CACHE
129        // /////////////////////////////////////////////////////////////////////////
130
131        private void clearCachedItemCount()
132        {
133                cachedItemCount = null;
134        }
135
136        // /////////////////////////////////////////////////////////////////////////
137        // PAGING
138        // /////////////////////////////////////////////////////////////////////////
139
140        /**
141         * @return maximum number of items that will be shown per page
142         */
143        @Override
144        public long getItemsPerPage()
145        {
146                return itemsPerPage;
147        }
148
149        /**
150         * Sets the maximum number of items to show per page. The current page will also be set to zero
151         * 
152         * @param items
153         */
154        @Override
155        public final void setItemsPerPage(long items)
156        {
157                if (items < 1)
158                {
159                        throw new IllegalArgumentException("Argument [itemsPerPage] cannot be less than 1");
160                }
161
162                if (itemsPerPage != items)
163                {
164                        if (isVersioned())
165                        {
166                                addStateChange();
167                        }
168                }
169
170                itemsPerPage = items;
171
172                // because items per page can effect the total number of pages we always
173                // reset the current page back to zero
174                setCurrentPage(0);
175        }
176
177        /**
178         * @return total item count
179         */
180        protected abstract long internalGetItemCount();
181
182        /**
183         * Get the row count.
184         * 
185         * @see #getItemCount()
186         * 
187         * @return total item count, but 0 if not visible in the hierarchy
188         */
189        public final long getRowCount()
190        {
191                if (!isVisibleInHierarchy())
192                {
193                        return 0;
194                }
195
196                return getItemCount();
197        }
198
199        /**
200         * Get the item count. Since dataprovider.size() could potentially be expensive, the item count
201         * is cached.
202         * 
203         * @see #getRowCount()
204         * 
205         * @return the item count
206         */
207        @Override
208        public final long getItemCount()
209        {
210                if (cachedItemCount != null)
211                {
212                        return cachedItemCount;
213                }
214
215                long count = internalGetItemCount();
216
217                cachedItemCount = count;
218                return count;
219        }
220
221        /**
222         * @see org.apache.wicket.markup.html.navigation.paging.IPageable#getCurrentPage()
223         */
224        @Override
225        public final long getCurrentPage()
226        {
227                long page = currentPage;
228
229                /*
230                 * trim current page if its out of bounds this can happen if items are added/deleted between
231                 * requests
232                 */
233
234                if (page > 0 && page >= getPageCount())
235                {
236                        page = Math.max(getPageCount() - 1, 0);
237                        currentPage = page;
238                        return page;
239                }
240
241                return page;
242        }
243
244        /**
245         * @see org.apache.wicket.markup.html.navigation.paging.IPageable#setCurrentPage(long)
246         */
247        @Override
248        public final void setCurrentPage(long page)
249        {
250                if (currentPage != page)
251                {
252                        if (isVersioned())
253                        {
254                                addStateChange();
255
256                        }
257                }
258                currentPage = page;
259        }
260
261        /**
262         * @see org.apache.wicket.markup.html.navigation.paging.IPageable#getPageCount()
263         */
264        @Override
265        public long getPageCount()
266        {
267                long total = getRowCount();
268                long itemsPerPage = getItemsPerPage();
269                long count = total / itemsPerPage;
270
271                if (itemsPerPage * count < total)
272                {
273                        count++;
274                }
275
276                return count;
277
278        }
279
280        /**
281         * @return the index of the first visible item in the view
282         */
283        public long getFirstItemOffset()
284        {
285                return getCurrentPage() * getItemsPerPage();
286        }
287
288
289        /**
290         * @return the number of items visible
291         */
292        public long getViewSize()
293        {
294                return Math.min(getItemsPerPage(), getRowCount() - getFirstItemOffset());
295        }
296
297        // /////////////////////////////////////////////////////////////////////////
298        // HELPER CLASSES
299        // /////////////////////////////////////////////////////////////////////////
300
301        /**
302         * Iterator adapter that makes sure only the specified max number of items can be accessed from
303         * its delegate.
304         * 
305         * @param <T>
306         *            Model object type
307         */
308        private static class CappedIteratorAdapter<T> implements Iterator<IModel<T>>
309        {
310                private final long max;
311                private long index;
312                private final Iterator<IModel<T>> delegate;
313
314                /**
315                 * Constructor
316                 * 
317                 * @param delegate
318                 *            delegate iterator
319                 * @param max
320                 *            maximum number of items that can be accessed.
321                 */
322                public CappedIteratorAdapter(Iterator<IModel<T>> delegate, long max)
323                {
324                        this.delegate = delegate;
325                        this.max = max;
326                }
327
328                /**
329                 * @see java.util.Iterator#remove()
330                 */
331                @Override
332                public void remove()
333                {
334                        throw new UnsupportedOperationException();
335                }
336
337                /**
338                 * @see java.util.Iterator#hasNext()
339                 */
340                @Override
341                public boolean hasNext()
342                {
343                        return (index < max) && delegate.hasNext();
344                }
345
346                /**
347                 * @see java.util.Iterator#next()
348                 */
349                @Override
350                public IModel<T> next()
351                {
352                        if (index >= max)
353                        {
354                                throw new NoSuchElementException();
355                        }
356                        index++;
357                        return delegate.next();
358                }
359
360        }
361
362        /**
363         * @see org.apache.wicket.Component#onDetach()
364         */
365        @Override
366        protected void onDetach()
367        {
368                clearCachedItemCount();
369                super.onDetach();
370        }
371}