package fr.openwide.core.wicket.more.bindable.model; import java.util.Collection; import java.util.Map; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.wicket.model.AbstractPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; import org.bindgen.BindingRoot; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Supplier; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import fr.openwide.core.commons.util.fieldpath.FieldPath; import fr.openwide.core.commons.util.fieldpath.FieldPathPropertyComponent; import fr.openwide.core.spring.util.StringUtils; import fr.openwide.core.wicket.more.bindable.exception.NoSuchModelException; import fr.openwide.core.wicket.more.model.WorkingCopyModel; import fr.openwide.core.wicket.more.util.model.Detachables; public class BindableModel<E> implements IBindableModel<E> { private static final long serialVersionUID = -1361726182147235268L; @SuppressWarnings("rawtypes") // Works for any T private static enum BindableModelWrappingFunction implements Function<IModel, IBindableModel> { INSTANCE; @Override @SuppressWarnings("unchecked") public IBindableModel apply(IModel input) { return input instanceof IBindableModel ? (IBindableModel) input : new BindableModel(input); } @SuppressWarnings("unchecked") private static <T> Function<? super IModel<T>, ? extends IBindableModel<T>> get() { // We are absolutely sure that the returned IBindableModel will be a IBindableModel<T> // We do two casts here to prevent the javac compiler to raise an error (it's a hack) return (Function<? super IModel<T>, ? extends IBindableModel<T>>) (Object) BindableModelWrappingFunction.INSTANCE; } } public static <T> Function<? super T, ? extends IBindableModel<T>> factory(Function<? super T, ? extends IModel<T>> function) { return Functions.compose(BindableModelWrappingFunction.<T>get(), function); } private IModel<E> mainModel; private IModel<E> initialValueModel; private transient boolean detaching = false; /** * Map of bindable models for properties. * <ul> * <li>Key: the property itself, as a {@link FieldPath}. * <li>Value: the {@link BindableModel} representing the property (which might be a {@link BindableCollectionModel} or * a {@link BindableMapModel}. * * <p>This attribute is lazily-initialized in {@link #getPropertyModels()}. */ private Map<FieldPath, BindableModel<?>> propertyModels; /** * Map of bindable models impacted when one path is updated. * * <p>This attribute is lazily-initialized in {@link #getPropertyModelsByImpactingPaths()}. */ private Multimap<FieldPath, BindableModel<?>> propertyModelsByImpactingPaths; public BindableModel(IModel<E> mainModel) { super(); this.mainModel = mainModel; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof BindableModel)) { return false; } BindableModel<?> other = (BindableModel<?>) obj; return new EqualsBuilder() .append(mainModel, other.mainModel) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder() .append(mainModel) .build(); } @Override public E getObject() { return mainModel.getObject(); } @Override public void setObject(E object) { getDelegateModel().setObject(object); readAllExceptMainModel(); } private IModel<E> getDelegateModel() { return mainModel; } protected final IModel<E> getMainModelInternal() { return mainModel; } protected final void setMainModel(IModel<E> mainModel) { this.mainModel = mainModel; } @Override public IModel<E> getInitialValueModel() { return this.initialValueModel; } @Override public void setInitialValueModel(IModel<E> initialValueModel) { this.initialValueModel = initialValueModel; this.initialValueModel.setObject(this.mainModel.getObject()); } private Map<FieldPath, BindableModel<?>> getPropertyModels() { if (propertyModels == null) { propertyModels = Maps.newLinkedHashMap(); } return propertyModels; } private Multimap<FieldPath, BindableModel<?>> getPropertyModelsByImpactingPaths() { if (propertyModelsByImpactingPaths == null) { propertyModelsByImpactingPaths = LinkedHashMultimap.create(); } return propertyModelsByImpactingPaths; } protected boolean hasCache() { return mainModel instanceof WorkingCopyModel; } @Override public <T> IBindableModel<T> bind(BindingRoot<? super E, T> binding) { return getOrCreateSimpleModel(binding, FieldPath.fromBinding(binding)); } @Override public <T> IBindableModel<T> bindWithCache(BindingRoot<? super E, T> binding, IModel<T> workingCopyProposal) { FieldPath path = FieldPath.fromBinding(binding); BindableModel<T> propertyModel = getOrCreateSimpleModel(binding, path); if (propertyModel.hasCache()) { return propertyModel; } else { WorkingCopyModel<T> workingCopyModel = WorkingCopyModel.of( propertyModel.getDelegateModel(), workingCopyProposal ); /* Add a cache to the property model, which may or may not have already existed before we called * getOrCreateSimpleModel(). */ propertyModel.setMainModel(workingCopyModel); registerImpactingPaths(path, propertyModel); return propertyModel; } } /** * Records the impacting paths, i.e. the paths such that when we read the cache on them (respectively, write), * then the given model should be read (respectively, written), too. * * @see #readAllUnder(BindingRoot) */ private void registerImpactingPaths(FieldPath path, BindableModel<?> model) { FieldPath currentPath = path; do { getPropertyModelsByImpactingPaths().put(currentPath, model); currentPath = currentPath.parent().get(); } while (!currentPath.isRoot()); } @Override public <T, C extends Collection<T>> IBindableCollectionModel<T, C> bindCollectionWithCache( BindingRoot<? super E, C> binding, Supplier<? extends C> newCollectionSupplier, Function<? super T, ? extends IModel<T>> itemModelFunction) { FieldPath path = FieldPath.fromBinding(binding); return bindCollectionWithCache(this, path, path, newCollectionSupplier, itemModelFunction); } private <T, C extends Collection<T>> IBindableCollectionModel<T, C> bindCollectionWithCache( BindableModel<?> rootBindableModel, FieldPath originalPath, FieldPath path, Supplier<? extends C> newCollectionSupplier, Function<? super T, ? extends IModel<T>> itemModelFunction) { BindableModel<?> owner = getOrCreateSimpleModel(path.parent().get()); if (owner != this) { return owner.bindCollectionWithCache( rootBindableModel, originalPath, path.relativeToParent().get(), newCollectionSupplier, itemModelFunction ); } @SuppressWarnings("unchecked") // Generic parameters are known, by construction BindableModel<C> propertyModel = (BindableModel<C>) getPropertyModels().get(path); if (propertyModel == null) { BindableCollectionModel<T, C> collectionPropertyModel = new BindableCollectionModel<>( this.<C>createBindingModel(getDelegateModel(), path), newCollectionSupplier, itemModelFunction ); getPropertyModels().put(path, collectionPropertyModel); rootBindableModel.registerImpactingPaths(originalPath, collectionPropertyModel); return collectionPropertyModel; } else if (!(propertyModel instanceof IBindableCollectionModel)) { // TODO YRO support the case when we first called bind(), then bindCollectionWithCache() throw newNotCollectionModelException(path); } else { @SuppressWarnings("unchecked") // Generic parameters are known, by construction. IBindableCollectionModel<T, C> collectionPropertyModel = (IBindableCollectionModel<T, C>) propertyModel; return collectionPropertyModel; } } private static IllegalStateException newNotCollectionModelException(FieldPath path) { return new IllegalStateException( "The model bound to property '" + path + "' was registered using bind() or bindWithCache(). Thus, it" + " does not not implement IBindableCollectionModel. This state cannot be reversed." + " You should check your code and make sure you will always call bindCollectionWithCache() first," + " before any call to bind() or bindWithCache() for the same property." ); } @Override public <T, C extends Collection<T>> IBindableCollectionModel<T, C> bindCollectionAlreadyAdded(BindingRoot<? super E, C> binding) { FieldPath path = FieldPath.fromBinding(binding); return bindCollectionAlreadyAdded(path); } private <T, C extends Collection<T>> IBindableCollectionModel<T, C> bindCollectionAlreadyAdded(FieldPath path) { BindableModel<?> owner = getOrCreateSimpleModel(path.parent().get()); if (owner != this) { return owner.bindCollectionAlreadyAdded(path.relativeToParent().get()); } @SuppressWarnings("unchecked") // Generic parameters are known, by construction BindableModel<C> propertyModel = (BindableModel<C>) getPropertyModels().get(path); if (propertyModel == null) { throw new NoSuchModelException("No collection model was added for path '" + path + "'. Use bindCollectionWithCache in order to add a collection model."); } else if (!(propertyModel instanceof IBindableCollectionModel)) { throw newNotCollectionModelException(path); } else { @SuppressWarnings("unchecked") // Generic parameters are known, by construction IBindableCollectionModel<T, C> collectionPropertyModel = (IBindableCollectionModel<T, C>) propertyModel; return collectionPropertyModel; } } @Override public <K, V, M extends Map<K, V>> IBindableMapModel<K, V, M> bindMapWithCache(BindingRoot<? super E, M> binding, Supplier<? extends M> newMapSupplier, Function<? super K, ? extends IModel<K>> keyModelFunction, Function<? super V, ? extends IModel<V>> valueModelFunction) { FieldPath path = FieldPath.fromBinding(binding); return bindMapWithCache(this, path, path, newMapSupplier, keyModelFunction, valueModelFunction); } private <K, V, M extends Map<K, V>> IBindableMapModel<K, V, M> bindMapWithCache(BindableModel<?> rootBindableModel, FieldPath originalPath, FieldPath path, Supplier<? extends M> newMapSupplier, Function<? super K, ? extends IModel<K>> keyModelFunction, Function<? super V, ? extends IModel<V>> valueModelFunction) { BindableModel<?> owner = getOrCreateSimpleModel(path.parent().get()); if (owner != this) { return owner.bindMapWithCache( rootBindableModel, originalPath, path.relativeToParent().get(), newMapSupplier, keyModelFunction, valueModelFunction ); } @SuppressWarnings("unchecked") // Generic parameters are known, by construction BindableModel<M> propertyModel = (BindableModel<M>) getPropertyModels().get(path); if (propertyModel == null) { BindableMapModel<K, V, M> mapPropertyModel = new BindableMapModel<>( this.<M>createBindingModel(getDelegateModel(), path), newMapSupplier, keyModelFunction, valueModelFunction ); getPropertyModels().put(path, mapPropertyModel); rootBindableModel.registerImpactingPaths(originalPath, mapPropertyModel); return mapPropertyModel; } else if (!(propertyModel instanceof IBindableMapModel)) { // TODO YRO support the case when we first called bind(), then bindMapWithCache() throw newNotMapModelException(path); } else { @SuppressWarnings("unchecked") // Generic parameters are known, by construction IBindableMapModel<K, V, M> mapPropertyModel = (IBindableMapModel<K, V, M>) propertyModel; return mapPropertyModel; } } private static IllegalStateException newNotMapModelException(FieldPath path) { return new IllegalStateException( "The model bound to property '" + path + "' was registered using bind() or bindWithCache(). Thus, it" + " does not not implement IBindableMapModel. This state cannot be reversed." + " You should check your code and make sure you will always call bindMapWithCache() first," + " before any call to bind() or bindWithCache() for the same property." ); } @Override public <K, V, M extends Map<K, V>> IBindableMapModel<K, V, M> bindMapAlreadyAdded(BindingRoot<? super E, M> binding) { FieldPath path = FieldPath.fromBinding(binding); return bindMapAlreadyAdded(path); } private <K, V, M extends Map<K, V>> IBindableMapModel<K, V, M> bindMapAlreadyAdded(FieldPath path) { BindableModel<?> owner = getOrCreateSimpleModel(path.parent().get()); if (owner != this) { return owner.bindMapAlreadyAdded(path.relativeToParent().get()); } @SuppressWarnings("unchecked") // Generic parameters are known, by construction IBindableModel<M> propertyModel = (IBindableModel<M>) getPropertyModels().get(path); if (propertyModel == null) { throw new NoSuchModelException("No map model was added for path '" + path + "'. Use bindMapWithCache in order to add a map model."); } else if (!(propertyModel instanceof IBindableMapModel)) { throw newNotMapModelException(path); } else { @SuppressWarnings("unchecked") // Generic parameters are known, by construction IBindableMapModel<K, V, M> mapPropertyModel = (IBindableMapModel<K, V, M>) propertyModel; return mapPropertyModel; } } /** * Retrieves a pre-existing BindableModel, or create it if necessary. * <p>If creation is necessary, the created model will be an instance of {@link BindableModel}, * not {@link BindableCollectionModel} or {@link BindableMapModel}. */ private <T> BindableModel<T> getOrCreateSimpleModel(BindingRoot<? super E, T> binding, FieldPath path) { return getOrCreateSimpleModel(path); } @SuppressWarnings("unchecked") private <T> BindableModel<T> getOrCreateSimpleModel(FieldPath path) { if (path.isRoot()) { // Binding the root, i.e. this return (BindableModel<T>) this; } else if (path.parent().get().isRoot()) { // Binding a direct child property BindableModel<T> propertyModel = (BindableModel<T>) getPropertyModels().get(path); if (propertyModel == null) { propertyModel = new BindableModel<>(this.<T>createBindingModel(getDelegateModel(), path)); getPropertyModels().put(path, propertyModel); } return propertyModel; } else { /* * Binding an indirect child property * We make sure to never create a "shortcut" that would skip an intermediary model, because * it would allow users to create multiple BindableModels pointing to the same property. */ FieldPath parentPath = path.parent().get(); FieldPath relativePropertyPath = path.relativeTo(parentPath).get(); return getOrCreateSimpleModel(parentPath) .getOrCreateSimpleModel(relativePropertyPath); } } private <T> AbstractPropertyModel<T> createBindingModel(IModel<E> delegateModel, FieldPath path) { final String propertyExpression = StringUtils.trimLeadingCharacter( path.toString(), FieldPathPropertyComponent.PROPERTY_SEPARATOR_CHAR ); return new PropertyModel<T>(delegateModel, propertyExpression); } @Override public final void write() { if (hasCache()) { ((WorkingCopyModel<?>)mainModel).write(); } } @Override public final void writeAll() { write(); if (propertyModels != null // Avoid an error in Wicket property resolver ("Runtime exception when trying to set a value on a null object.") && getObject() != null ) { for (IBindableModel<?> propertyModel : propertyModels.values()) { propertyModel.writeAll(); } } onWriteAll(); } protected void onWriteAll() { // nothing to do } @Override public final void readAll() { if (hasCache()) { ((WorkingCopyModel<?>)mainModel).read(); } readAllExceptMainModel(); } @Override public final void readAllExceptMainModel() { if (propertyModels != null) { for (IBindableModel<?> propertyModel : propertyModels.values()) { propertyModel.readAll(); } } onReadAll(); } protected void onReadAll() { // nothing to do } @Override public final <T2> void readAllUnder(BindingRoot<? super E, T2> binding) { if (propertyModelsByImpactingPaths != null) { for (BindableModel<?> propertyModel : propertyModelsByImpactingPaths.get(FieldPath.fromBinding(binding))) { propertyModel.readAll(); } } } @Override public final void detach() { if (!detaching) { detaching = true; try { try { onDetach(); } finally { mainModel.detach(); Detachables.detach(initialValueModel); if (propertyModels != null) { Detachables.detach(propertyModels.values()); } } } finally { detaching = false; } } } protected void onDetach() { // To be overridden by subclasses } }