This is an automated email from the ASF dual-hosted git repository.
reiern70 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/wicket.git
The following commit(s) were added to refs/heads/master by this push:
new f6cfc1dc56 Seal LoadableDetachableModel#detach() and add object-aware
detach hooks
f6cfc1dc56 is described below
commit f6cfc1dc56cb49f8a628e837d97e2a701d675f90
Author: reiern70 <[email protected]>
AuthorDate: Wed Sep 23 09:57:28 2026 -0500
Seal LoadableDetachableModel#detach() and add object-aware detach hooks
detach() drives the model's attach/detach state machine: it invokes
onDetach() when there is something to detach, then discards the transient
object and resets the state. It was overridable, so a subclass could run
cleanup on either side of super.detach(), or skip the super call and leave
the model attached for good. Nothing enforced the invariant the method
exists to maintain.
detach() is now final, and cleanup goes into one of two hooks:
onDetach() - as before, only when the model was attached
onDetachAlways() - on every detach() call, attached or not
onDetachAlways() is what an override of detach() in practice was, and is
where cleanup not tied to the loaded object belongs: detaching models this
one was handed, for instance, which may have been attached without this
model ever loading.
A subclass that needed the loaded object while detaching had to keep a
reference of its own, because onDetach() took no arguments and the field
holding the object is private. The new onDetach(T object) is handed that
object before it is discarded. The default onDetach() delegates to it, so
an override of onDetach() that does not call super suppresses it.
Making a public method final is source-incompatible, which is why this
lands on master only. It stays binary compatible - detach() still resolves,
on LoadableDetachableModel - so compiled subclasses keep running, but a
subclass that overrides detach() no longer compiles. Moving the override's
body to onDetachAlways() and dropping the super.detach() call reproduces
the old behaviour exactly; moving it to onDetach() narrows it to the
attached case. That rewrite needs a judgement about which hook applies, so
it is documented as a manual step in wicket.yml rather than automated.
Three subclasses in the tree overrode detach(), each to detach something
unconditionally, and all three move to onDetachAlways(): StringResourceModel
and its AssignmentWrapper, and the devutils SessionIdentifiersModel.
StringResourceModel is the reason onDetachAlways() exists at all - per
WICKET-5176 it has to detach its substitution models even when it was never
attached itself, which an attached-only hook cannot do. Its override stays
final, as detach() was, so a subclass cannot quietly drop that cleanup.
GitHub issue #1614: https://github.com/apache/wicket/issues/1614
---
.../wicket/model/LoadableDetachableModelTest.java | 112 +++++++++++++++++++++
.../wicket/model/LoadableDetachableModel.java | 41 +++++++-
.../apache/wicket/model/StringResourceModel.java | 23 +++--
.../pagestore/browser/SessionIdentifiersModel.java | 4 +-
.../src/main/resources/META-INF/rewrite/wicket.yml | 8 ++
5 files changed, 175 insertions(+), 13 deletions(-)
diff --git
a/wicket-core-tests/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
b/wicket-core-tests/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
index 6035a083de..8c23939123 100644
---
a/wicket-core-tests/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
+++
b/wicket-core-tests/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
@@ -18,6 +18,7 @@ package org.apache.wicket.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayInputStream;
@@ -134,6 +135,117 @@ class LoadableDetachableModelTest extends WicketTestCase
assertEquals(true, ldm.detachCalled);
}
+ @Test
+ void onDetachReceivesTheAttachedObject()
+ {
+ class DetachingLoad extends LoadableDetachableModel<Integer>
+ {
+ private static final long serialVersionUID = 1L;
+
+ private boolean detachCalled = false;
+
+ private Integer detached;
+
+ @Override
+ protected Integer load()
+ {
+ return 42;
+ }
+
+ @Override
+ protected void onDetach(Integer object)
+ {
+ detachCalled = true;
+ detached = object;
+ }
+ }
+
+ DetachingLoad ldm = new DetachingLoad();
+ assertThat(ldm.getObject()).isEqualTo(42);
+
+ ldm.detach();
+
+ assertEquals(true, ldm.detachCalled);
+ assertThat(ldm.detached).isEqualTo(42);
+ assertEquals(false, ldm.isAttached());
+ }
+
+ @Test
+ void onDetachReceivesNullAfterFailedLoad()
+ {
+ class ExceptionalLoad extends LoadableDetachableModel<Integer>
+ {
+ private static final long serialVersionUID = 1L;
+
+ private boolean detachCalled = false;
+
+ private Integer detached = 42;
+
+ @Override
+ protected Integer load()
+ {
+ throw new RuntimeException();
+ }
+
+ @Override
+ protected void onDetach(Integer object)
+ {
+ detachCalled = true;
+ detached = object;
+ }
+ }
+
+ ExceptionalLoad ldm = new ExceptionalLoad();
+ assertThrows(RuntimeException.class, ldm::getObject);
+
+ ldm.detach();
+
+ assertEquals(true, ldm.detachCalled);
+ assertThat(ldm.detached).isNull();
+ }
+
+ @Test
+ void onDetachAlwaysCalledEvenWhenNeverAttached()
+ {
+ class CountingLoad extends LoadableDetachableModel<Integer>
+ {
+ private static final long serialVersionUID = 1L;
+
+ private int detachCount = 0;
+
+ private int alwaysCount = 0;
+
+ @Override
+ protected Integer load()
+ {
+ return 42;
+ }
+
+ @Override
+ protected void onDetach(Integer object)
+ {
+ detachCount++;
+ }
+
+ @Override
+ protected void onDetachAlways()
+ {
+ alwaysCount++;
+ }
+ }
+
+ CountingLoad ldm = new CountingLoad();
+
+ ldm.detach();
+ assertEquals(0, ldm.detachCount);
+ assertEquals(1, ldm.alwaysCount);
+
+ ldm.getObject();
+ ldm.detach();
+ assertEquals(1, ldm.detachCount);
+ assertEquals(2, ldm.alwaysCount);
+ }
+
private static class SerializedLoad extends
LoadableDetachableModel<Integer>
{
private static final long serialVersionUID = 1L;
diff --git
a/wicket-core/src/main/java/org/apache/wicket/model/LoadableDetachableModel.java
b/wicket-core/src/main/java/org/apache/wicket/model/LoadableDetachableModel.java
index 872a202ba2..d36464046e 100644
---
a/wicket-core/src/main/java/org/apache/wicket/model/LoadableDetachableModel.java
+++
b/wicket-core/src/main/java/org/apache/wicket/model/LoadableDetachableModel.java
@@ -98,7 +98,7 @@ public abstract class LoadableDetachableModel<T> implements
IModel<T>
}
@Override
- public void detach()
+ public final void detach()
{
// even if LDM is in partial attached state (ATTACHING) it
should be detached
if (state != null && state != InternalState.DETACHED)
@@ -115,6 +115,8 @@ public abstract class LoadableDetachableModel<T> implements
IModel<T>
log.debug("removed transient object for '{}'",
this);
}
}
+
+ onDetachAlways();
}
@Override
@@ -178,8 +180,45 @@ public abstract class LoadableDetachableModel<T>
implements IModel<T>
/**
* Detaches from the current request. Implement this method with custom
behavior, such as
* setting the model object to null.
+ * <p>
+ * This implementation delegates to {@link #onDetach(Object)}, passing
the object this model is
+ * currently holding. An override that does not call {@code
super.onDetach()} suppresses that
+ * callback.
*/
protected void onDetach()
+ {
+ onDetach(transientModelObject);
+ }
+
+ /**
+ * Detaches from the current request, handing over the object this
model is holding before it is
+ * discarded. This is the place to release resources tied to that
object.
+ * <p>
+ * The object is the one returned by the last {@link #load()} or passed
to
+ * {@link #setObject(Object)}. It is {@code null} when that value was
{@code null}, and also when
+ * the model is detached while still attaching - {@link #load()} is
then either running or has
+ * thrown.
+ *
+ * @param object
+ * the object that was attached, may be {@code null}
+ * @since 11.0.0
+ */
+ protected void onDetach(T object)
+ {
+ }
+
+ /**
+ * Detaches from the current request, whether this model was attached
or not. Unlike
+ * {@link #onDetach()} this is invoked on every call to {@link
#detach()}, so it is the place for
+ * cleanup that is not tied to the loaded object - detaching models
this one was handed, for
+ * instance, which may have been attached without this model ever
loading.
+ * <p>
+ * When the model was attached, this runs after {@link #onDetach()} and
after the loaded object
+ * has been discarded.
+ *
+ * @since 11.0.0
+ */
+ protected void onDetachAlways()
{
}
diff --git
a/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java
b/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java
index 2364fa2aa3..8aaeb3f16c 100644
--- a/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java
+++ b/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java
@@ -241,23 +241,26 @@ public class StringResourceModel extends
LoadableDetachableModel<String>
this.component = component;
}
- @Override
- public void detach()
- {
- super.detach();
-
- StringResourceModel.this.detach();
- }
-
@Override
protected void onDetach()
{
+ super.onDetach();
+
if (StringResourceModel.this.component == null)
{
+ // without an explicit component the wrapped
model never attaches itself
StringResourceModel.this.onDetach();
}
}
+ @Override
+ protected void onDetachAlways()
+ {
+ super.onDetachAlways();
+
+ StringResourceModel.this.detach();
+ }
+
@Override
protected String load()
{
@@ -619,9 +622,9 @@ public class StringResourceModel extends
LoadableDetachableModel<String>
}
@Override
- public final void detach()
+ protected final void onDetachAlways()
{
- super.detach();
+ super.onDetachAlways();
// detach any model
if (model != null)
diff --git
a/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java
b/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java
index 7cd8b4c189..b7183582ca 100644
---
a/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java
+++
b/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java
@@ -63,9 +63,9 @@ public class SessionIdentifiersModel extends
LoadableDetachableModel<List<String
}
@Override
- public void detach()
+ protected void onDetachAlways()
{
- super.detach();
+ super.onDetachAlways();
store.detach();
}
diff --git a/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml
b/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml
index 90b1b3a735..7bd0e14305 100644
--- a/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml
+++ b/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml
@@ -81,6 +81,14 @@ recipeList:
# The ChangeType entries below handle the common case; getRememberMe(),
setRememberMe(),
# onSignInRemembered() and the two-argument SignInPanel constructor have no
equivalent and need
# hand-editing. Both are deprecated in 8.19.0, 9.24.0 and 10.11.0.
+#
+# org.apache.wicket.model.LoadableDetachableModel#detach() is final, so a
subclass can no longer
+# step into the attach/detach state machine. An override of it moves its body
to one of two hooks
+# and drops the super.detach() call: onDetachAlways(), which runs on every
detach() just as the
+# override did, or onDetach(), which runs only when the model was attached.
The new onDetach(T) is
+# handed the object the model was holding before that object is discarded; the
default onDetach()
+# delegates to it, so an override of onDetach() that does not call super
suppresses it. These
+# require manual migration.
type: specs.openrewrite.org/v1beta/recipe
name: org.apache.wicket.MigrateToWicket11
displayName: Migrate to Wicket 11.x