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: */
017: package org.apache.wicket.markup.html.list;
018:
019: import java.io.Serializable;
020: import java.util.Collections;
021: import java.util.Iterator;
022: import java.util.List;
023:
024: import org.apache.wicket.Component;
025: import org.apache.wicket.markup.html.link.Link;
026: import org.apache.wicket.markup.repeater.AbstractRepeater;
027: import org.apache.wicket.model.IModel;
028: import org.apache.wicket.model.Model;
029: import org.apache.wicket.util.collections.ReadOnlyIterator;
030: import org.apache.wicket.version.undo.Change;
031:
032: /**
033: * A ListView is a repeater that makes it easy to display/work with {@link List}s.
034: * However, there are situations where it is necessary to work with other
035: * collection types, for repeaters that might work better with non-list or
036: * database-driven collections see the org.apache.wicket.markup.repeater
037: * package.
038: *
039: * Also notice that in a list the item's uniqueness/primary key/id is identified
040: * as its index in the list. If this is not the case you should either override
041: * {@link #getListItemModel(IModel, int)} to return a model that will work with
042: * the item's true primary key, or use a different repeater that does not rely
043: * on the list index.
044: *
045: *
046: * A ListView holds ListItem children. Items can be re-ordered and deleted,
047: * either one at a time or many at a time.
048: * <p>
049: * Example:
050: *
051: * <pre>
052: * <tbody>
053: * <tr wicket:id="rows" class="even">
054: * <td><span wicket:id="id">Test ID</span></td>
055: * ...
056: * </pre>
057: *
058: * <p>
059: * Though this example is about a HTML table, ListView is not at all limited to
060: * HTML tables. Any kind of list can be rendered using ListView.
061: * <p>
062: * The related Java code:
063: *
064: * <pre>
065: * add(new ListView("rows", listData)
066: * {
067: * public void populateItem(final ListItem item)
068: * {
069: * final UserDetails user = (UserDetails)item.getModelObject();
070: * item.add(new Label("id", user.getId()));
071: * }
072: * });
073: * </pre>
074: *
075: * <p>
076: * <strong>NOTE:</strong>
077: *
078: * When you want to change the default generated markup it is important to
079: * realise that the ListView instance itself does not correspond to any markup,
080: * however, the generated ListItems do.<br/>
081: *
082: * This means that methods like {@link #setRenderBodyOnly(boolean)} and
083: * {@link #add(org.apache.wicket.behavior.IBehavior)} should be invoked on the
084: * {@link ListItem} that is given in {@link #populateItem(ListItem)} method.
085: * </p>
086: *
087: * <p>
088: * <strong>WARNING:</strong> though you can nest ListViews within Forms, you
089: * HAVE to set the setReuseItems property to true in order to have validation
090: * work properly. By default, setReuseItems is false, which has the effect that
091: * ListView replaces all child components by new instances. The idea behind this
092: * is that you always render the fresh data, and as people usually use ListViews
093: * for displaying read-only lists (at least, that's what we think), this is good
094: * default behavior. <br />
095: * However, as the components are replaced before the rendering starts, the
096: * search for specific messages for these components fails as they are replaced
097: * with other instances. Another problem is that 'wrong' user input is kept as
098: * (temporary) instance data of the components. As these components are replaced
099: * by new ones, your user will never see the wrong data when setReuseItems is
100: * false.
101: * </p>
102: *
103: * @author Jonathan Locke
104: * @author Juergen Donnerstag
105: * @author Johan Compagner
106: * @author Eelco Hillenius
107: */
108: public abstract class ListView extends AbstractRepeater {
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
114: * doesn't get changed at all or if it gets scrolled (compared to paging).
115: * But if you modify the listView model object, than you must manually call
116: * listView.removeAll() in order to rebuild the ListItems. If you nest a
117: * ListView in a Form, ALWAYS set this property to true, as otherwise
118: * validation will not work properly.
119: */
120: private boolean reuseItems = false;
121:
122: /** Max number (not index) of items to show */
123: private int viewSize = Integer.MAX_VALUE;
124:
125: /**
126: * @see org.apache.wicket.Component#Component(String)
127: */
128: public ListView(final String id) {
129: super (id);
130: }
131:
132: /**
133: * @see org.apache.wicket.Component#Component(String, IModel)
134: */
135: public ListView(final String id, final IModel model) {
136: super (id, model);
137:
138: if (model == null) {
139: throw new IllegalArgumentException(
140: "Null models are not allowed. If you have no model, you may prefer a Loop instead");
141: }
142:
143: // A reasonable default for viewSize can not be determined right now,
144: // because list items might be added or removed until ListView
145: // gets rendered.
146: }
147:
148: /**
149: * @param id
150: * See Component
151: * @param list
152: * List to cast to Serializable
153: * @see org.apache.wicket.Component#Component(String, IModel)
154: */
155: public ListView(final String id, final List list) {
156: this (id, new Model((Serializable) list));
157: }
158:
159: /**
160: * Gets the list of items in the listView. This method is final because it
161: * is not designed to be overridden. If it were allowed to be overridden,
162: * the values returned by getModelObject() and getList() might not coincide.
163: *
164: * @return The list of items in this list view.
165: */
166: public final List getList() {
167: final List list = (List) getModelObject();
168: if (list == null) {
169: return Collections.EMPTY_LIST;
170: }
171: return list;
172: }
173:
174: /**
175: * If true re-rendering the list view is more efficient if the windows
176: * doesn't get changed at all or if it gets scrolled (compared to paging).
177: * But if you modify the listView model object, than you must manually call
178: * listView.removeAll() in order to rebuild the ListItems. If you nest a
179: * ListView in a Form, ALLWAYS set this property to true, as otherwise
180: * validation will not work properly.
181: *
182: * @return Whether to reuse items
183: */
184: public boolean getReuseItems() {
185: return reuseItems;
186: }
187:
188: /**
189: * Get index of first cell in page. Default is: 0.
190: *
191: * @return Index of first cell in page. Default is: 0
192: */
193: public final int getStartIndex() {
194: return firstIndex;
195: }
196:
197: /**
198: * Based on the model object's list size, firstIndex and view size,
199: * determine what the view size really will be. E.g. default for viewSize is
200: * Integer.MAX_VALUE, if not set via setViewSize(). If the underlying list
201: * has 10 elements, the value returned by getViewSize() will be 10 if
202: * startIndex = 0.
203: *
204: * @return The number of items to be populated and rendered.
205: */
206: public int getViewSize() {
207: int size = viewSize;
208:
209: final Object modelObject = getModelObject();
210: if (modelObject == null) {
211: return size == Integer.MAX_VALUE ? 0 : size;
212: }
213:
214: // Adjust view size to model object's list size
215: final int modelSize = getList().size();
216: if (firstIndex > modelSize) {
217: return 0;
218: }
219:
220: if ((size == Integer.MAX_VALUE)
221: || ((firstIndex + size) > modelSize)) {
222: size = modelSize - firstIndex;
223: }
224:
225: // firstIndex + size must be smaller than Integer.MAX_VALUE
226: if ((Integer.MAX_VALUE - size) < firstIndex) {
227: throw new IllegalStateException(
228: "firstIndex + size must be smaller than Integer.MAX_VALUE");
229: }
230:
231: return size;
232: }
233:
234: /**
235: * Returns a link that will move the given item "down" (towards the end) in
236: * the listView.
237: *
238: * @param id
239: * Name of move-down link component to create
240: * @param item
241: * @return The link component
242: */
243: public final Link moveDownLink(final String id, final ListItem item) {
244: return new Link(id) {
245: private static final long serialVersionUID = 1L;
246:
247: /**
248: * @see org.apache.wicket.markup.html.link.Link#onClick()
249: */
250: public void onClick() {
251: final int index = getList().indexOf(
252: item.getModelObject());
253: if (index != -1) {
254: addStateChange(new Change() {
255: private static final long serialVersionUID = 1L;
256:
257: final int oldIndex = index;
258:
259: public void undo() {
260: Collections.swap(getList(), oldIndex + 1,
261: oldIndex);
262: }
263:
264: });
265:
266: // Swap list items and invalidate listView
267: Collections.swap(getList(), index, index + 1);
268: ListView.this .removeAll();
269: }
270: }
271:
272: /**
273: * @see org.apache.wicket.Component#onBeforeRender()
274: */
275: protected void onBeforeRender() {
276: super .onBeforeRender();
277: setAutoEnable(false);
278: if (getList().indexOf(item.getModelObject()) == (getList()
279: .size() - 1)) {
280: setEnabled(false);
281: }
282: }
283: };
284: }
285:
286: /**
287: * Returns a link that will move the given item "up" (towards the beginning)
288: * in the listView.
289: *
290: * @param id
291: * Name of move-up link component to create
292: * @param item
293: * @return The link component
294: */
295: public final Link moveUpLink(final String id, final ListItem item) {
296: return new Link(id) {
297: private static final long serialVersionUID = 1L;
298:
299: /**
300: * @see org.apache.wicket.markup.html.link.Link#onClick()
301: */
302: public void onClick() {
303: final int index = getList().indexOf(
304: item.getModelObject());
305: if (index != -1) {
306:
307: addStateChange(new Change() {
308: private static final long serialVersionUID = 1L;
309:
310: final int oldIndex = index;
311:
312: public void undo() {
313: Collections.swap(getList(), oldIndex - 1,
314: oldIndex);
315: }
316:
317: });
318:
319: // Swap items and invalidate listView
320: Collections.swap(getList(), index, index - 1);
321: ListView.this .removeAll();
322: }
323: }
324:
325: /**
326: * @see org.apache.wicket.Component#onBeforeRender()
327: */
328: protected void onBeforeRender() {
329: super .onBeforeRender();
330: setAutoEnable(false);
331: if (getList().indexOf(item.getModelObject()) == 0) {
332: setEnabled(false);
333: }
334: }
335: };
336: }
337:
338: /**
339: * Returns a link that will remove this ListItem from the ListView that
340: * holds it.
341: *
342: * @param id
343: * Name of remove link component to create
344: * @param item
345: * @return The link component
346: */
347: public final Link removeLink(final String id, final ListItem item) {
348: return new Link(id) {
349: private static final long serialVersionUID = 1L;
350:
351: /**
352: * @see org.apache.wicket.markup.html.link.Link#onClick()
353: */
354: public void onClick() {
355: addStateChange(new Change() {
356: private static final long serialVersionUID = 1L;
357:
358: final int oldIndex = getList().indexOf(
359: item.getModelObject());
360: final Object removedObject = item.getModelObject();
361:
362: public void undo() {
363: getList().add(oldIndex, removedObject);
364: }
365:
366: });
367:
368: item.modelChanging();
369:
370: // Remove item and invalidate listView
371: getList().remove(item.getModelObject());
372:
373: ListView.this .modelChanged();
374: ListView.this .removeAll();
375: }
376: };
377: }
378:
379: /**
380: * Sets the model as the provided list and removes all children, so that the
381: * next render will be using the contents of the model.
382: *
383: * @param list
384: * The list for the new model. The list must implement
385: * {@link Serializable}.
386: * @return This for chaining
387: */
388: public Component setList(List list) {
389: return setModel(new Model((Serializable) list));
390: }
391:
392: /**
393: * Sets the model and removes all current children, so that the next render
394: * will be using the contents of the model.
395: *
396: * @param model
397: * The new model
398: * @return This for chaining
399: *
400: * @see org.apache.wicket.MarkupContainer#setModel(org.apache.wicket.model.IModel)
401: */
402: public Component setModel(IModel model) {
403: return super .setModel(model);
404: }
405:
406: /**
407: * If true re-rendering the list view is more efficient if the windows
408: * doesn't get changed at all or if it gets scrolled (compared to paging).
409: * But if you modify the listView model object, than you must manually call
410: * listView.removeAll() in order to rebuild the ListItems. If you nest a
411: * ListView in a Form, ALLWAYS set this property to true, as otherwise
412: * validation will not work properly.
413: *
414: * @param reuseItems
415: * Whether to reuse the child items.
416: * @return this
417: */
418: public ListView setReuseItems(boolean reuseItems) {
419: this .reuseItems = reuseItems;
420: return this ;
421: }
422:
423: /**
424: * Set the index of the first item to render
425: *
426: * @param startIndex
427: * First index of model object's list to display
428: * @return This
429: */
430: public ListView setStartIndex(final int startIndex) {
431: firstIndex = startIndex;
432:
433: if (firstIndex < 0) {
434: firstIndex = 0;
435: } else if (firstIndex > getList().size()) {
436: firstIndex = 0;
437: }
438:
439: return this ;
440: }
441:
442: /**
443: * Define the maximum number of items to render. Default: render all.
444: *
445: * @param size
446: * Number of items to display
447: * @return This
448: */
449: public ListView setViewSize(final int size) {
450: viewSize = size;
451:
452: if (viewSize < 0) {
453: viewSize = Integer.MAX_VALUE;
454: }
455:
456: return this ;
457: }
458:
459: /**
460: * Subclasses may provide their own ListItemModel with extended
461: * functionality. The default ListItemModel works fine with mostly static
462: * lists where index remains valid. In cases where the underlying list
463: * changes a lot (many users using the application), it may not longer be
464: * appropriate. In that case your own ListItemModel implementation should
465: * use an id (e.g. the database' record id) to identify and load the list
466: * item model object.
467: *
468: * @param listViewModel
469: * The ListView's model
470: * @param index
471: * The list item index
472: * @return The ListItemModel created
473: */
474: protected IModel getListItemModel(final IModel listViewModel,
475: final int index) {
476: return new ListItemModel(this , index);
477: }
478:
479: /**
480: * Create a new ListItem for list item at index.
481: *
482: * @param index
483: * @return ListItem
484: */
485: protected ListItem newItem(final int index) {
486: return new ListItem(index, getListItemModel(getModel(), index));
487: }
488:
489: /**
490: * @see org.apache.wicket.MarkupContainer#onBeforeRender()
491: */
492: protected void onBeforeRender() {
493: if (isVisibleInHierarchy()) {
494: // Get number of items to be displayed
495: final int size = getViewSize();
496: if (size > 0) {
497: if (getReuseItems()) {
498: // Remove all ListItems no longer required
499: final int maxIndex = firstIndex + size;
500: for (final Iterator iterator = iterator(); iterator
501: .hasNext();) {
502: // Get next child component
503: final ListItem child = (ListItem) iterator
504: .next();
505: if (child != null) {
506: final int index = child.getIndex();
507: if (index < firstIndex || index >= maxIndex) {
508: iterator.remove();
509: }
510: }
511: }
512: } else {
513: // Automatically rebuild all ListItems before rendering the
514: // list view
515: removeAll();
516: }
517:
518: // Loop through the markup in this container for each item
519: for (int i = 0; i < size; i++) {
520: // Get index
521: final int index = firstIndex + i;
522:
523: // If this component does not already exist, populate it
524: ListItem item = (ListItem) get(Integer
525: .toString(index));
526: if (item == null) {
527: // Create item for index
528: item = newItem(index);
529:
530: // Add list item
531: add(item);
532:
533: // Populate the list item
534: onBeginPopulateItem(item);
535: populateItem(item);
536: }
537: }
538: } else {
539: removeAll();
540: }
541:
542: }
543: super .onBeforeRender();
544: }
545:
546: /**
547: * Comes handy for ready made ListView based components which must implement
548: * populateItem() but you don't want to lose compile time error checking
549: * reminding the user to implement abstract populateItem().
550: *
551: * @param item
552: */
553: protected void onBeginPopulateItem(final ListItem item) {
554: }
555:
556: /**
557: * Populate a given item.
558: * <p>
559: * <b>be carefull</b> to add any components to the list item. So, don't do:
560: *
561: * <pre>
562: * add(new Label("foo", "bar"));
563: * </pre>
564: *
565: * but:
566: *
567: * <pre>
568: * item.add(new Label("foo", "bar"));
569: * </pre>
570: *
571: * </p>
572: *
573: * @param item
574: * The item to populate
575: */
576: protected abstract void populateItem(final ListItem item);
577:
578: /**
579: * @see org.apache.wicket.markup.repeater.AbstractRepeater#renderChild(org.apache.wicket.Component)
580: */
581: protected final void renderChild(Component child) {
582: renderItem((ListItem) child);
583: }
584:
585: /**
586: * Render a single item.
587: *
588: * @param item
589: * The item to be rendered
590: */
591: protected void renderItem(final ListItem item) {
592: item.render(getMarkupStream());
593: }
594:
595: /**
596: * @see org.apache.wicket.markup.repeater.AbstractRepeater#renderIterator()
597: */
598: protected Iterator renderIterator() {
599:
600: final int size = size();
601: return new ReadOnlyIterator() {
602: private int index = 0;
603:
604: public boolean hasNext() {
605: return index < size;
606: }
607:
608: public Object next() {
609: final String id = Integer.toString(firstIndex + index);
610: index++;
611: Component c = get(id);
612: return c;
613: }
614: };
615: }
616: }
|