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.navigation.paging;
018
019import java.util.Map;
020
021import org.apache.wicket.Component;
022import org.apache.wicket.behavior.Behavior;
023import org.apache.wicket.markup.ComponentTag;
024import org.apache.wicket.markup.html.basic.Label;
025import org.apache.wicket.markup.html.link.AbstractLink;
026import org.apache.wicket.markup.html.list.Loop;
027import org.apache.wicket.markup.html.list.LoopItem;
028import org.apache.wicket.model.Model;
029import org.apache.wicket.util.collections.MicroMap;
030
031/**
032 * A navigation for a PageableListView that holds links to other pages of the PageableListView.
033 * <p>
034 * For each row (one page of the list of pages) a {@link PagingNavigationLink}will be added that
035 * contains a {@link Label} with the page number of that link (1..n).
036 *
037 * <pre>
038 *
039 *      &lt;td wicket:id=&quot;navigation&quot;&gt;
040 *              &lt;a wicket:id=&quot;pageLink&quot; href=&quot;SearchCDPage.html&quot;&gt;
041 *                      &lt;span wicket:id=&quot;pageNumber&quot;&gt;1&lt;/span&gt;
042 *              &lt;/a&gt;
043 *      &lt;/td&gt;
044 *
045 * </pre>
046 *
047 * thus renders like:
048 *
049 * <pre>
050 *      1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
051 * </pre>
052 *
053 * </p>
054 * <p>
055 * Override method populateItem to customize the rendering of the navigation. For instance:
056 *
057 * <pre>
058 * protected void populateItem(LoopItem loopItem)
059 * {
060 *      final int page = loopItem.getIteration();
061 *      final PagingNavigationLink link = new PagingNavigationLink(&quot;pageLink&quot;, pageableListView, page);
062 *      if (page &gt; 0)
063 *      {
064 *              loopItem.add(new Label(&quot;separator&quot;, &quot;|&quot;));
065 *      }
066 *      else
067 *      {
068 *              loopItem.add(new Label(&quot;separator&quot;, &quot;&quot;));
069 *      }
070 *      link.add(new Label(&quot;pageNumber&quot;, String.valueOf(page + 1)));
071 *      link.add(new Label(&quot;pageLabel&quot;, &quot;page&quot;));
072 *      loopItem.add(link);
073 * }
074 * </pre>
075 *
076 * With:
077 *
078 * <pre>
079 * &lt;span wicket:id=&quot;navigation&quot;&gt;
080 *      &lt;span wicket:id=&quot;separator&quot;&gt;&lt;/span&gt;
081 *      &lt;a wicket:id=&quot;pageLink&quot; href=&quot;#&quot;&gt;
082 *              &lt;span wicket:id=&quot;pageLabel&quot;&gt;&lt;/span&gt;&lt;span wicket:id=&quot;pageNumber&quot;&gt;&lt;/span&gt;
083 *      &lt;/a&gt;
084 * &lt;/span&gt;
085 * </pre>
086 *
087 * renders like:
088 *
089 * <pre>
090 * page1 | page2 | page3 | page4 | page5 | page6 | page7 | page8 | page9
091 * </pre>
092 *
093 * </p>
094 * Assuming a PageableListView with 1000 entries and not more than 10 lines shall be printed per
095 * page, the navigation bar would have 100 entries. Because this is not feasible PagingNavigation's
096 * navigation bar is pageable as well.
097 * <p>
098 * The page links displayed are automatically adjusted based on the number of page links to be
099 * displayed and a margin. The margin makes sure that the page link pointing to the current page is
100 * not at the left or right end of the page links currently printed and thus providing a better user
101 * experience.
102 * <p>
103 * Use setMargin() and setViewSize() to adjust the navigation's bar view size and margin.
104 * <p>
105 * Please
106 *
107 * @see PagingNavigator for a ready made component which already includes links to the first,
108 *      previous, next and last page.
109 *
110 * @author Jonathan Locke
111 * @author Eelco Hillenius
112 * @author Juergen Donnerstag
113 */
114public class PagingNavigation extends Loop
115{
116        private static final long serialVersionUID = 1L;
117
118        /** The PageableListView this navigation is navigating. */
119        protected IPageable pageable;
120
121        /** The label provider for the text that the links should be displaying. */
122        protected IPagingLabelProvider labelProvider;
123
124        /** Offset for the Loop */
125        private long startIndex;
126
127        /**
128         * Number of links on the left and/or right to keep the current page link somewhere near the
129         * middle.
130         */
131        private long margin = -1;
132
133        /** Default separator between page numbers. Null: no separator. */
134        private String separator = null;
135
136        /**
137         * The maximum number of page links to show.
138         */
139        private int viewSize = 10;
140
141
142        /**
143         * Constructor.
144         *
145         * @param id
146         *            See Component
147         * @param pageable
148         *            The underlying pageable component to navigate
149         */
150        public PagingNavigation(final String id, final IPageable pageable)
151        {
152                this(id, pageable, null);
153        }
154
155        /**
156         * Constructor.
157         *
158         * @param id
159         *            See Component
160         * @param pageable
161         *            The underlying pageable component to navigate
162         * @param labelProvider
163         *            The label provider for the text that the links should be displaying.
164         */
165        public PagingNavigation(final String id, final IPageable pageable,
166                final IPagingLabelProvider labelProvider)
167        {
168                super(id, null);
169                this.pageable = pageable;
170                this.labelProvider = labelProvider;
171                startIndex = 0;
172        }
173
174        /**
175         * Gets the margin, default value is half the view size, unless explicitly set.
176         *
177         * @return the margin
178         */
179        public long getMargin()
180        {
181                if (margin == -1 && viewSize != 0)
182                {
183                        return viewSize / 2;
184                }
185                return margin;
186        }
187
188        /**
189         * Gets the seperator.
190         *
191         * @return the seperator
192         */
193        public String getSeparator()
194        {
195                return separator;
196        }
197
198        /**
199         * Gets the view size (is fixed by user).
200         *
201         * @return view size
202         */
203        public int getViewSize()
204        {
205                return viewSize;
206        }
207
208        /**
209         * view size of the navigation bar.
210         *
211         * @param size
212         */
213        public void setViewSize(final int size)
214        {
215                viewSize = size;
216        }
217
218        /**
219         * Sets the margin.
220         *
221         * @param margin
222         *            the margin
223         */
224        public void setMargin(final int margin)
225        {
226                this.margin = margin;
227        }
228
229        /**
230         * Sets the seperator. Null meaning, no separator at all.
231         *
232         * @param separator
233         *            the seperator
234         */
235        public void setSeparator(final String separator)
236        {
237                this.separator = separator;
238        }
239
240        @Override
241        protected void onConfigure()
242        {
243                super.onConfigure();
244                setDefaultModel(new Model<Integer>(
245                        (int)Math.max(Integer.MAX_VALUE, pageable.getPageCount())));
246                // PagingNavigation itself (as well as the PageableListView)
247                // may have pages.
248
249                // The index of the first page link depends on the PageableListView's
250                // page currently printed.
251                setStartIndex();
252        }
253
254        /**
255         * Allow subclasses replacing populateItem to calculate the current page number
256         *
257         * @return start index
258         */
259        protected final long getStartIndex()
260        {
261                return startIndex;
262        }
263
264        /**
265         * Populate the current cell with a page link (PagingNavigationLink) enclosing the page number
266         * the link is pointing to. Subclasses may provide there own implementation adding more
267         * sophisticated page links.
268         *
269         * @see org.apache.wicket.markup.html.list.Loop#populateItem(org.apache.wicket.markup.html.list.LoopItem)
270         */
271        @Override
272        protected void populateItem(final LoopItem loopItem)
273        {
274                // Get the index of page this link shall point to
275                final long pageIndex = getStartIndex() + loopItem.getIndex();
276
277                // Add a page link pointing to the page
278                final AbstractLink link = newPagingNavigationLink("pageLink", pageable, pageIndex);
279                link.add(new TitleAppender(pageIndex));
280                loopItem.add(link);
281
282                // Add a page number label to the list which is enclosed by the link
283                String label = "";
284                if (labelProvider != null)
285                {
286                        label = labelProvider.getPageLabel(pageIndex);
287                }
288                else
289                {
290                        label = String.valueOf(pageIndex + 1).intern();
291                }
292                link.add(new Label("pageNumber", label));
293        }
294
295        /**
296         * Factory method for creating page number links.
297         *
298         * @param id
299         *            the component id.
300         * @param pageable
301         *            the pageable for the link
302         * @param pageIndex
303         *            the page index the link points to
304         * @return the page navigation link.
305         */
306        protected AbstractLink newPagingNavigationLink(String id, IPageable pageable, long pageIndex)
307        {
308                return new PagingNavigationLink<Void>(id, pageable, pageIndex);
309        }
310
311        /**
312         * Renders the page link. Add the separator if not the last page link
313         *
314         * @see Loop#renderItem(org.apache.wicket.markup.html.list.LoopItem)
315         */
316        @Override
317        protected void renderItem(final LoopItem loopItem)
318        {
319                // Call default implementation
320                super.renderItem(loopItem);
321
322                // Add separator if not last page
323                if (separator != null && (loopItem.getIndex() != getIterations() - 1))
324                {
325                        getResponse().write(separator);
326                }
327        }
328
329        /**
330         * Get the first page link to render. Adjust the first page link based on the current
331         * PageableListView page displayed.
332         */
333        private void setStartIndex()
334        {
335                // Which startIndex are we currently using
336                long firstListItem = startIndex;
337
338                // How many page links shall be displayed
339                int viewSize = (int)Math.min(getViewSize(), pageable.getPageCount());
340                long margin = getMargin();
341
342                // What is the PageableListView's page index to be displayed
343                long currentPage = pageable.getCurrentPage();
344
345                // Make sure the current page link index is within the current
346                // window taking the left and right margin into account
347                if (currentPage < (firstListItem + margin))
348                {
349                        firstListItem = currentPage - margin;
350                }
351                else if ((currentPage >= (firstListItem + viewSize - margin)))
352                {
353
354                        firstListItem = (currentPage + margin + 1) - viewSize;
355                }
356
357                // Make sure the first index is >= 0 and the last index is <=
358                // than the last page link index.
359                if ((firstListItem + viewSize) >= pageable.getPageCount())
360                {
361                        firstListItem = pageable.getPageCount() - viewSize;
362                }
363
364                if (firstListItem < 0)
365                {
366                        firstListItem = 0;
367                }
368
369                if ((viewSize != getIterations()) || (startIndex != firstListItem))
370                {
371                        modelChanging();
372
373                        // Tell the ListView what the new start index shall be
374                        addStateChange();
375                        startIndex = firstListItem;
376
377                        setIterations((int)Math.min(viewSize, pageable.getPageCount()));
378
379                        modelChanged();
380
381                        // force all children to be re-rendered
382                        removeAll();
383                }
384        }
385
386        /**
387         * Set the number of iterations.
388         *
389         * @param i
390         *            the number of iterations
391         */
392        private void setIterations(int i)
393        {
394                setDefaultModelObject(i);
395        }
396
397        /**
398         * Appends title attribute to navigation links
399         *
400         * @author igor.vaynberg
401         */
402        private final class TitleAppender extends Behavior
403        {
404                private static final long serialVersionUID = 1L;
405                /** resource key for the message */
406                private static final String RES = "PagingNavigation.page";
407                /** page number */
408                private final long page;
409
410                /**
411                 * Constructor
412                 *
413                 * @param page
414                 *            page number to use as the ${page} var
415                 */
416                public TitleAppender(long page)
417                {
418                        this.page = page;
419                }
420
421                /** {@inheritDoc} */
422                @Override
423                public void onComponentTag(Component component, ComponentTag tag)
424                {
425                        String pageIndex = String.valueOf(page + 1).intern();
426                        Map<String, String> vars = Map.of("page", pageIndex);
427                        tag.put("title", PagingNavigation.this.getString(RES, Model.ofMap(vars)));
428                }
429        }
430}