WICKET-5916 StackOverflowError in LoadableDetachableModel

This fixes a circular calling issue with LoadableDetachableModel where
getObject() is called from the context of a load(). By tracking the
state of the LoadableDetachableModel more closely, we can short-circuit
the call chain and break out of the infinite calling loop.

I've opted to use an enum instead of adding a new flag to keep the
amount of flags to a minimum and to keep the memory requirements
low (instead of 2 flags, just one enum is referenced). The external
public and protected interfaces are still the same, so I don't
expect any problems within user applications, unless they are
manipulating the attached flag using reflection.

Fixes WICKET-5916


Project: http://git-wip-us.apache.org/repos/asf/wicket/repo
Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/5049aa85
Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/5049aa85
Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/5049aa85

Branch: refs/heads/WICKET-5906-7.x
Commit: 5049aa8525e6ee268c3e78ec77dc7012da13e51c
Parents: 55cfd27
Author: Martijn Dashorst <martijn.dasho...@gmail.com>
Authored: Fri May 29 19:11:28 2015 +0200
Committer: Andrea Del Bene <“adelb...@apache.org”>
Committed: Thu Jun 4 16:19:29 2015 +0200

----------------------------------------------------------------------
 .../wicket/model/LoadableDetachableModel.java   | 65 +++++++++++----
 .../model/LoadableDetachableModelTest.java      | 87 ++++++++++++++++++++
 2 files changed, 138 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/wicket/blob/5049aa85/wicket-core/src/main/java/org/apache/wicket/model/LoadableDetachableModel.java
----------------------------------------------------------------------
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 c172678..e3a7fc0 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
@@ -23,8 +23,8 @@ import org.slf4j.LoggerFactory;
 
 /**
  * Model that makes working with detachable models a breeze. 
LoadableDetachableModel holds a
- * temporary, transient model object, that is set when {@link #getObject()} is 
called by
- * calling abstract method 'load', and that will be reset/ set to null on 
{@link #detach()}.
+ * temporary, transient model object, that is set when {@link #getObject()} is 
called by calling
+ * abstract method 'load', and that will be reset/ set to null on {@link 
#detach()}.
  * 
  * A usage example:
  * 
@@ -60,8 +60,40 @@ public abstract class LoadableDetachableModel<T> implements 
IModel<T>
        /** Logger. */
        private static final Logger log = 
LoggerFactory.getLogger(LoadableDetachableModel.class);
 
+       private enum AttachingState 
+       {
+               DETACHED(false, false),
+               ATTACHING(true, false), 
+               ATTACHED(true, true);
+
+               private boolean attaching;
+               private boolean attached;
+
+               private AttachingState(boolean attaching, boolean attached)
+               {
+                       this.attached = attached;
+                       this.attaching = attaching;
+               }
+               
+               public boolean isAttached() 
+               {
+                       return attached;
+               }
+
+               public boolean isAttaching() 
+               {
+                       return attaching;
+               }
+               
+               @Override
+               public String toString()
+               {
+                       return name().toLowerCase();
+               }
+       }
+
        /** keeps track of whether this model is attached or detached */
-       private transient boolean attached = false;
+       private transient AttachingState attached = AttachingState.DETACHED;
 
        /** temporary, transient object. */
        private transient T transientModelObject;
@@ -83,7 +115,7 @@ public abstract class LoadableDetachableModel<T> implements 
IModel<T>
        public LoadableDetachableModel(T object)
        {
                this.transientModelObject = object;
-               attached = true;
+               attached = AttachingState.ATTACHED;
        }
 
        /**
@@ -92,7 +124,7 @@ public abstract class LoadableDetachableModel<T> implements 
IModel<T>
        @Override
        public void detach()
        {
-               if (attached)
+               if (attached == AttachingState.ATTACHED)
                {
                        try
                        {
@@ -100,7 +132,7 @@ public abstract class LoadableDetachableModel<T> implements 
IModel<T>
                        }
                        finally
                        {
-                               attached = false;
+                               attached = AttachingState.DETACHED;
                                transientModelObject = null;
 
                                log.debug("removed transient object for {}, 
requestCycle {}", this,
@@ -115,8 +147,11 @@ public abstract class LoadableDetachableModel<T> 
implements IModel<T>
        @Override
        public final T getObject()
        {
-               if (!attached)
+               if (attached == AttachingState.DETACHED)
                {
+                       // prevent infinite attachment loops
+                       attached = AttachingState.ATTACHING;
+
                        transientModelObject = load();
 
                        if (log.isDebugEnabled())
@@ -125,7 +160,7 @@ public abstract class LoadableDetachableModel<T> implements 
IModel<T>
                                        ", requestCycle " + RequestCycle.get());
                        }
 
-                       attached = true;
+                       attached = AttachingState.ATTACHED;
                        onAttach();
                }
                return transientModelObject;
@@ -138,7 +173,7 @@ public abstract class LoadableDetachableModel<T> implements 
IModel<T>
         */
        public final boolean isAttached()
        {
-               return attached;
+               return attached.isAttached();
        }
 
        /**
@@ -147,9 +182,12 @@ public abstract class LoadableDetachableModel<T> 
implements IModel<T>
        @Override
        public String toString()
        {
-        StringBuilder sb = new StringBuilder(super.toString());
-               
sb.append(":attached=").append(attached).append(":tempModelObject=[").append(
-                       this.transientModelObject).append("]");
+               StringBuilder sb = new StringBuilder(super.toString());
+               sb.append(":attached=")
+                       .append(isAttached())
+                       .append(":tempModelObject=[")
+                       .append(this.transientModelObject)
+                       .append("]");
                return sb.toString();
        }
 
@@ -187,8 +225,7 @@ public abstract class LoadableDetachableModel<T> implements 
IModel<T>
        @Override
        public void setObject(final T object)
        {
-               attached = true;
+               attached = AttachingState.ATTACHED;
                transientModelObject = object;
        }
-
 }

http://git-wip-us.apache.org/repos/asf/wicket/blob/5049aa85/wicket-core/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
----------------------------------------------------------------------
diff --git 
a/wicket-core/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
 
b/wicket-core/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
new file mode 100644
index 0000000..a087a78
--- /dev/null
+++ 
b/wicket-core/src/test/java/org/apache/wicket/model/LoadableDetachableModelTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.model;
+
+import static org.hamcrest.core.Is.is;
+
+import org.apache.wicket.WicketTestCase;
+import org.junit.Test;
+
+/**
+ * Tests the states of a LoadableDetachableModel
+ */
+public class LoadableDetachableModelTest extends WicketTestCase
+{
+       /**
+        * Checks whether the LDM can escape recursive calls.
+        */
+       @Test
+       public void recursiveGetObjectDoesntCauseInfiteLoop()
+       {
+               class RecursiveLoad extends LoadableDetachableModel<Integer>
+               {
+                       private static final long serialVersionUID = 1L;
+
+                       private int count = 0;
+
+                       @Override
+                       protected Integer load()
+                       {
+                               count++;
+                               getObject();
+                               return count;
+                       }
+               }
+
+               RecursiveLoad ldm = new RecursiveLoad();
+
+               assertThat(ldm.isAttached(), is(false));
+               assertThat(ldm.getObject(), is(1));
+               assertThat(ldm.isAttached(), is(true));
+       }
+
+       /**
+        * Checks whether the LDM can escape recursive calls.
+        */
+       @Test
+       public void exceptionDuringLoadKeepsLDMDetached()
+       {
+               class ExceptionalLoad extends LoadableDetachableModel<Integer>
+               {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       protected Integer load()
+                       {
+                               throw new RuntimeException();
+                       }
+               }
+
+               ExceptionalLoad ldm = new ExceptionalLoad();
+
+               assertThat(ldm.isAttached(), is(false));
+               try
+               {
+                       assertThat(ldm.getObject(), is(1));
+                       fail("shouldn't get here");
+               }
+               catch (RuntimeException e)
+               {
+                       assertThat(ldm.isAttached(), is(false));
+               }
+       }
+}

Reply via email to