package lt.inventi.wicket.component.repeater.expandable; import java.io.Serializable; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.wicket.Component; import org.apache.wicket.IGenericComponent; import org.apache.wicket.MarkupContainer; import org.apache.wicket.markup.repeater.IItemFactory; import org.apache.wicket.markup.repeater.IItemReuseStrategy; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.RefreshingView; import org.apache.wicket.markup.repeater.ReuseIfModelsEqualStrategy; import org.apache.wicket.model.ChainingModel; import org.apache.wicket.model.IModel; import org.apache.wicket.util.lang.Generics; /** * A refreshing view which allows adding/removing rows on the fly. * <p> * If the model is bound to a non-random access collection (e.g. Set, * Collection), the view will not replace model objects inside of the collection * during the model update. * <p> * Ids are counted from zero (not from 1) and maintains same IDs whenever the * view is refreshed. * <p> * If you use add/remove links, make sure you have this view nested to parent * container which should include new rows. For example in case if its table, * then this view (which would be bound to <code>tr</code>) must be nested in * container, which is <code>tbody</code>. * */ public abstract class ExpandableView<T> extends RefreshingView<T> implements IGenericComponent<List<T>> { private int childIdCounter = 0; public ExpandableView(String id) { super(id); } public ExpandableView(String id, IModel<? extends List<T>> model) { super(id, model); } @Override protected void onInitialize() { setItemReuseStrategy(ExpandableReuseIfModelsEqualStrategy.instance); super.onInitialize(); } @Override protected Iterator<IModel<T>> getItemModels() { return modelIterator(getModel()); } @Override public MarkupContainer remove(Component component) { if (component instanceof Item) { @SuppressWarnings("unchecked") Item<T> item = (Item<T>) component; Iterator<Item<T>> allItems = getItems(); while (allItems.hasNext()) { Item<T> nextItem = allItems.next(); getTrackingModel(nextItem.getModel()).onRemove(item.getIndex()); } } return super.remove(component); } @Override public String newChildId() { String id = String.valueOf(childIdCounter); childIdCounter++; return id; } @Override @SuppressWarnings("unchecked") public IModel<List<T>> getModel() { return (IModel<List<T>>) getDefaultModel(); } @Override public void setModel(IModel<List<T>> model) { setDefaultModel(model); } @Override public void setModelObject(List<T> object) { setModelObject(object); } @Override public List<T> getModelObject() { return getModel().getObject(); } @Override protected Item<T> newItem(String id, int index, IModel<T> model) { Item<T> newItem = super.newItem(id, index, model); newItem.setOutputMarkupId(true); return newItem; } @SuppressWarnings("unchecked") // internal use from AddNewItemLink Item<T> appendAndGetNewItem(T newItem) { getModel().getObject().add(newItem); onPopulate(); return (Item<T>) get(getModel().getObject().size() - 1); } private Iterator<IModel<T>> modelIterator(IModel<? extends List<T>> model) { if (model == null) { return new Iterator<IModel<T>>() { @Override public boolean hasNext() { return false; } @Override public IModel<T> next() { throw new IllegalStateException("No element!"); } @Override public void remove() { unsupportedRemoval(); } }; } return new ListModelIterator(); } private class ListModelIterator implements Iterator<IModel<T>>, Serializable { private int index = 0; @Override public boolean hasNext() { return index < getList().size(); } @Override public IModel<T> next() { IndexTrackingModel<T> model = new IndexTrackingModel<T>(index) { @Override public T getObject() { List<T> list = getList(); int index = Math.min(Math.max(getIndex(), 0), list.size() - 1); return list.get(index); } @Override public void setObject(T object) { getList().set(getIndex(), object); } }; index++; return model; } @Override public void remove() { unsupportedRemoval(); } @SuppressWarnings("unchecked") private List<T> getList() { return (List<T>) ExpandableView.this.getDefaultModelObject(); } } private static abstract class IndexTrackingModel<T> implements IModel<T> { private int index; public IndexTrackingModel(int index) { this.index = index; } @Override public void detach() { // nothing } public int getIndex() { return index; } public void onRemove(int removedIndex) { if (removedIndex < index) { index--; } } @Override public int hashCode() { return index; } @Override public boolean equals(Object obj) { if (!(obj instanceof IModel)) { return false; } try { @SuppressWarnings("unchecked") IndexTrackingModel<T> model = getTrackingModel((IModel<T>) obj); return model.index == this.index; } catch (IllegalStateException e) { return false; } } } /** * Adapted for expandable view. * * @see ReuseIfModelsEqualStrategy */ private static class ExpandableReuseIfModelsEqualStrategy implements IItemReuseStrategy { private static IItemReuseStrategy instance = new ExpandableReuseIfModelsEqualStrategy(); @Override public <T> Iterator<Item<T>> getItems(final IItemFactory<T> factory, final Iterator<IModel<T>> newModels, Iterator<Item<T>> existingItems) { final Map<IModel<T>, Item<T>> modelToItem = Generics.newHashMap(); while (existingItems.hasNext()) { final Item<T> item = existingItems.next(); modelToItem.put(getTrackingModel(item.getModel()), item); } return new Iterator<Item<T>>() { private int index = 0; @Override public boolean hasNext() { return newModels.hasNext(); } @Override public Item<T> next() { final IModel<T> model = newModels.next(); final Item<T> oldItem = modelToItem.get(model); final Item<T> item; if (oldItem == null) { item = factory.newItem(index, model); } else { oldItem.setIndex(index); item = oldItem; } index++; return item; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } } private static void unsupportedRemoval() { throw new UnsupportedOperationException("Cannot remove items from ExpandableView!"); } @SuppressWarnings({ "unchecked", "rawtypes" }) private static <T> IndexTrackingModel<T> getTrackingModel(IModel<T> model) { IModel<T> current = model; while (!(current instanceof IndexTrackingModel)) { if (current instanceof ChainingModel) { current = ((ChainingModel) current).getChainedModel(); } else { throw new IllegalStateException("Please do not unwrap ExpandableView models!"); } } return (IndexTrackingModel<T>) current; } }