package fr.openwide.core.wicket.more.model;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.javatuples.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Maps;
import fr.openwide.core.wicket.more.markup.repeater.collection.IItemModelAwareCollectionModel;
import fr.openwide.core.wicket.more.markup.repeater.map.IItemModelAwareMapModel;
import fr.openwide.core.wicket.more.markup.repeater.map.IMapModel;
import fr.openwide.core.wicket.more.util.model.Detachables;
import fr.openwide.core.wicket.more.util.model.Models;
/**
* An abstract base for implementations of {@link IMapModel} whose content is to be "cloned" (i.e. copied to a new
* map) each time {@link #setObject(Map)} is called.
*
* <p>This is typically what you want when editing a map in a form.
*
* <p>Content is stored as a map of element models. Thus, subclasses of this one are guaranteed to always return the
* same model for a given element (key or value), up to this element's removal from the map.
*
* <p><strong>WARNING:</strong> this model is only intended to contain small maps. It is absolutely not optimized
* for large maps (say, more than just one or two dozens of items). Performance issues may arise when dealing
* with large maps.
*/
abstract class AbstractMapCopyModel<K, V, M extends Map<K, V>, MK extends IModel<K>, MV extends IModel<V>>
extends LoadableDetachableModel<M>
implements IItemModelAwareMapModel<K, V, M, MK, MV> {
private static final long serialVersionUID = 8313241207877097043L;
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMapCopyModel.class);
private final Map<MK, MV> modelMap = Maps.newLinkedHashMap();
public AbstractMapCopyModel() {
super();
}
@Override
public void detach() {
if (!isAttached()) {
/*
* Make sure the models are given a chance to process post-detach changes on their object.
* Useful in particular for GenericEntityModel.detach()
*/
Detachables.detach(modelMap);
return;
} else {
super.detach();
}
}
@Override
protected void onDetach() {
updateModels();
Detachables.detach(modelMap);
}
protected final void updateModelsIfExternalChangeIsPossible() {
if (isAttached()) {
updateModels();
}
}
private void updateModels() {
// Save the previous models for re-use based on the key model's current content
Map<K, Pair<MK, MV>> oldModels = Maps.newHashMap();
for (Map.Entry<MK, MV> entry : modelMap.entrySet()) {
K key = entry.getKey().getObject();
Object previousModelsForThisKey = oldModels.put(key, Pair.with(entry.getKey(), entry.getValue()));
if (previousModelsForThisKey != null) {
LOGGER.warn(
"Detected multiple models for the key {} in {}. One key/value pair might have been lost while"
+ " updating models."
+ " You probably get this warning because you called setObject() on a key model, which is"
+ " not recommended.",
key, modelMap
);
}
}
// Build the model map based on the current object
modelMap.clear();
Map<K, V> currentObject = getObject();
for (Entry<K, V> item : currentObject.entrySet()) {
Pair<MK, MV> existingPair = oldModels.get(item.getKey());
MK keyModel;
MV valueModel;
if (existingPair != null) {
// Re-use existing models
keyModel = existingPair.getValue0();
valueModel = existingPair.getValue1();
V oldValue = valueModel.getObject();
V newValue = item.getValue();
if (!Objects.equals(oldValue, newValue)) {
valueModel.setObject(newValue);
}
} else {
// Otherwise, create new models
keyModel = createKeyModel(item.getKey());
valueModel = createValueModel(item.getValue());
}
modelMap.put(keyModel, valueModel);
}
}
/**
* WARNING: if the client calls <code>setObject(null)</code>, a subsequent call to <code>getObject()</code>
* will not return <code>null</code>, but <em>an empty map</em>.
*/
@Override
public final void setObject(M object) {
M map = createMap();
if (object != null) {
map.putAll(object);
}
super.setObject(map);
}
@Override
protected final M load() {
M map = createMap();
for (Entry<MK, MV> item : modelMap.entrySet()) {
map.put(item.getKey().getObject(), item.getValue().getObject());
}
return map;
}
@Override
public Iterator<MK> iterator() {
return keysModel().iterator();
}
@Override
public Iterator<MK> iterator(long offset, long limit) {
return keysModel().iterator(offset, limit);
}
@Override
public IItemModelAwareCollectionModel<K, Set<K>, MK> keysModel() {
return new KeysModel();
}
protected class KeysModel extends AbstractMapCollectionModel<K, Set<K>, MK> {
private static final long serialVersionUID = 1L;
@Override
public void detach() {
AbstractMapCopyModel.this.detach();
}
@Override
public Set<K> getObject() {
Map<K, ?> map = AbstractMapCopyModel.this.getObject();
return map == null ? null : map.keySet();
}
@Override
protected Iterable<MK> internalIterable() {
updateModelsIfExternalChangeIsPossible();
return modelMap.keySet();
}
}
@Override
public IItemModelAwareCollectionModel<V, Collection<V>, MV> valuesModel() {
return new ValuesModel();
}
protected class ValuesModel extends AbstractMapCollectionModel<V, Collection<V>, MV> {
private static final long serialVersionUID = 1L;
@Override
public void detach() {
AbstractMapCopyModel.this.detach();
}
@Override
public Collection<V> getObject() {
Map<?, V> map = AbstractMapCopyModel.this.getObject();
return map == null ? null : map.values();
}
@Override
protected Iterable<MV> internalIterable() {
updateModelsIfExternalChangeIsPossible();
return modelMap.values();
}
}
@Override
public final long size() {
updateModelsIfExternalChangeIsPossible();
return modelMap.size();
}
@Override
public final IModel<V> valueModel(final IModel<? extends K> keyModel) {
return Models.mapModelValueModel(this, keyModel);
}
@Override
public final MV valueModelForProvidedKeyModel(final IModel<K> keyModel) {
// We don't need to update the modelMap here, as the keyModel is supposed to have been provided by this object
return modelMap.get(keyModel);
}
@Override
public final void put(K key, V value) {
/*
* We must put to an instance of the underlying map, not to the model map, so as to respect the
* properties of this map (make the map itself pick the element to be replaced, for instance).
*/
M map = getObject();
map.put(key, value);
updateModelsIfExternalChangeIsPossible();
}
@Override
public final void remove(K key) {
/*
* We must remove from an instance of the underlying map, not from the model map, so as to respect the
* properties of this map (make the map itself pick the element to be removed).
*/
M map = getObject();
map.remove(key);
updateModelsIfExternalChangeIsPossible();
}
@Override
public final void clear() {
super.setObject(createMap()); // Remove the cached collection from LoadableDetachableModel
modelMap.clear();
detach();
}
protected abstract M createMap();
protected abstract MK createKeyModel(K key);
protected abstract MV createValueModel(V value);
}