package fr.openwide.core.wicket.more.model.threadsafe; import java.io.Serializable; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.wicket.Session; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.util.lang.Objects; import fr.openwide.core.wicket.more.model.threadsafe.impl.AbstractThreadSafeLoadableDetachableModel; import fr.openwide.core.wicket.more.model.threadsafe.impl.LoadableDetachableModelThreadContext; /** * An implementation of {@link LoadableDetachableModel} that is thread-safe, and may thus be used in multiple request cycles at the same time. * <p>This should be used mainly as an abstract base class for entity-related loadable/detachable models (such as {@link SessionThreadSafeGenericEntityModel}). * When in doubt, prefer {@link SessionThreadSafeSimpleLoadableDetachableModel} as an abstract base class for your custom thread-safe loadable/detachable model. * <p>This class may be used as a basis for loadable/detachable models when: * <ul> * <li>the model is stored in a global object, such as the {@link Session wicket session} * <li>the serializable state of this loadable/detachable model derives directly from the model object (see {@link #load(Serializable)} and {@link #makeSerializable(Object)}) * </ul> * * <p><strong>WARNING:</strong> user willing to make changes on this model (calls to {@link #setObject(Object)}) from multiple request cycles * should be advised that only the changes from the call to {@link #setObject(Object)} will be kept, except if the said object experienced changes affecting * its {@link #makeSerializable(Object) serialized form} afterwards. In this last case, the actual effect is well-defined, but quite complex and should probably not be relied on. * * <p>Actual changes to the stored serialized form of the model object will happen in the following situations:<ul> * <li>{@link #ThreadSafeLoadableDetachableModel(T)} is called * <li>{@link #setObject(T)} is called * <li>{@link #detach()} is called AND the model is attached AND the object returned by {@link #makeSerializable(T)} is not {@link #equals(Object) equal} to * the one that was returned by {@code makeSerializable(currentModelObject)} on the last call to {@link #ThreadSafeLoadableDetachableModel(Object)}, * {@link #setObject(Object)} or {@link #getObject()} (whichever happened last). * </ul> * * The last criteria ensures that changes to the model object impacting its serializable form will not be lost, even if they happen * after the last call to {@link #setObject(Object)}. It also ensures that threads that do <em>not</em> change the model object won't write it to the shared context. * * @param <T> The type of the model object. * @param <S> The type of the object that is serialized in stead of the model object. This type must be immutable. */ public abstract class SessionThreadSafeDerivedSerializableStateLoadableDetachableModel<T, S extends Serializable> extends AbstractThreadSafeLoadableDetachableModel<T, SessionThreadSafeDerivedSerializableStateLoadableDetachableModel<T,S>.ThreadContextImpl> { private static final long serialVersionUID = 6859907414385876596L; protected class ThreadContextImpl extends LoadableDetachableModelThreadContext<T> { /** * The last value of the shared serializable state, either when loading or when setting it. */ private S lastSharedSerializableState = null; } /** * The serializable form of the model object, shared among all threads. */ private final AtomicReference<S> sharedSerializableState = new AtomicReference<S>(); public SessionThreadSafeDerivedSerializableStateLoadableDetachableModel() { } public SessionThreadSafeDerivedSerializableStateLoadableDetachableModel(S serializableObject) { sharedSerializableState.set(serializableObject); } public SessionThreadSafeDerivedSerializableStateLoadableDetachableModel(T object) { setObject(object); } @Override protected final ThreadContextImpl newThreadContext() { return new ThreadContextImpl(); } @Override protected final T load(ThreadContextImpl threadContext) { threadContext.lastSharedSerializableState = sharedSerializableState.get(); return load(threadContext.lastSharedSerializableState); } /** * Loads the model object value from the implementation-defined data source, using given {@link #makeSerializable(Object) serializable state}. * @param serializableState The serializable state that was returned by {@link #makeSerializable(Object)}. * @see #makeSerializable(T) */ protected abstract T load(S serializableState); @Override protected final void onSetObject(ThreadContextImpl threadContext) { S serializableState = makeSerializable(threadContext.getTransientModelObject()); sharedSerializableState.set(serializableState); threadContext.lastSharedSerializableState = serializableState; save(threadContext.getTransientModelObject()); } protected void save(T object) { // Does nothing by default } /** * Creates the serializable state from the given object. * @return A serializable object that will be used in {@link #load(S)} to retrieve the object when it is not available anymore. * @see #load(S) */ protected abstract S makeSerializable(T currentObject); @Override protected final void onDetach(ThreadContextImpl threadContext) { S attachedObjectSerializableState = makeSerializable(threadContext.getTransientModelObject()); // Write to the shared context ONLY if a change occurred in this thread. // This prevents the model to overwrite a serializable object that has been set in another thread if there was no change in this thread. if (!Objects.equal(attachedObjectSerializableState, threadContext.lastSharedSerializableState)) { threadContext.lastSharedSerializableState = attachedObjectSerializableState; sharedSerializableState.set(attachedObjectSerializableState); } onDetach(); } @Override protected final void onDetachDetached() { S sharedState = sharedSerializableState.get(); S normalizedState = normalizeDetached(sharedState); // Write to the shared context ONLY if a change occurred in this thread. // This prevents the model to overwrite a serializable object that has been set in another thread if there was no change in this thread. if (!Objects.equal(normalizedState, sharedState)) { sharedSerializableState.set(normalizedState); // No need to update the threadContext here: we're in detached state. } } /** * Normalize the serializable state, optionnally replacing it. * <p>This method is called whenever {@code detach()} is called, but the model is already detached. This enables * performing additional checks whenever another object <strong>in the same thread</strong> might have altered * the serializable state. */ protected S normalizeDetached(S current) { return current; } protected void onDetach() { } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (!(obj instanceof SessionThreadSafeDerivedSerializableStateLoadableDetachableModel)) { return false; } SessionThreadSafeDerivedSerializableStateLoadableDetachableModel<?, ?> other = (SessionThreadSafeDerivedSerializableStateLoadableDetachableModel<?, ?>) obj; return new EqualsBuilder() .append(sharedSerializableState.get(), other.sharedSerializableState.get()) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder() .append(sharedSerializableState.get()) .toHashCode(); } }