Author: dan.j.allen
Date: 2008-11-17 00:15:05 -0500 (Mon, 17 Nov 2008)
New Revision: 9578
Modified:
trunk/src/main/org/jboss/seam/core/ConversationEntry.java
trunk/src/main/org/jboss/seam/persistence/ManagedEntityInterceptor.java
trunk/src/main/org/jboss/seam/persistence/ManagedEntityWrapper.java
trunk/src/test/integration/src/org/jboss/seam/test/integration/EntityPassivationTest.java
Log:
JBSEAM-2209
note that this commit includes formatting changes (explains some of the modified lines)
Modified: trunk/src/main/org/jboss/seam/core/ConversationEntry.java
===================================================================
--- trunk/src/main/org/jboss/seam/core/ConversationEntry.java 2008-11-16 10:11:21 UTC (rev
9577)
+++ trunk/src/main/org/jboss/seam/core/ConversationEntry.java 2008-11-17 05:15:05 UTC (rev
9578)
@@ -6,6 +6,11 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
+import org.jboss.seam.Component;
+import org.jboss.seam.ScopeType;
+import org.jboss.seam.contexts.Context;
+import org.jboss.seam.contexts.Contexts;
+
/**
* Metadata about an active conversation. Also used
* by the conversation list and breadcrumbs.
@@ -245,6 +250,43 @@
{
return conversationIdStack.size()>1;
}
+
+ /**
+ * Determines which conversation in the stack is holding the instance of this
+ * component. A nested conversation can see context variables in all ancestor
+ * conversations. In this case, we are interesting in knowing where that
+ * instance was found. We are assuming that if the reference is not in an
+ * ancestor conversation, then it must be in the current conversation. The
+ * goal here is not to locate the instance, but rather to determine which
+ * conversation is contributing the instance that we already know exists.
+ *
+ * The low-level interaction with the session context should be refactored
+ * out. The problem is that it is defined in private areas of
+ * ServerConversationContext and cannot be reused. Actually, what we really
+ * need is a general purpose utility for analyzing the contents of each
+ * conversation in the stack (at least the keys).
+ */
+ public String findPositionInConversationStack(Component component)
+ {
+ if (component.isPerNestedConversation()) {
+ return id;
+ }
+
+ String name = component.getName();
+ Context session = Contexts.getSessionContext();
+ String location = id;
+ for (int i = 1, len = conversationIdStack.size(); i < len; i++) {
+ String cid = conversationIdStack.get(i);
+ String key = ScopeType.CONVERSATION.getPrefix() + '#' + cid +
'$' + name;
+ if (session.get(key) != null) {
+ location = cid;
+ break;
+ }
+ }
+
+ return location;
+ }
+
@Override
public String toString()
{
Modified: trunk/src/main/org/jboss/seam/persistence/ManagedEntityInterceptor.java
===================================================================
--- trunk/src/main/org/jboss/seam/persistence/ManagedEntityInterceptor.java 2008-11-16
10:11:21 UTC (rev 9577)
+++ trunk/src/main/org/jboss/seam/persistence/ManagedEntityInterceptor.java 2008-11-17
05:15:05 UTC (rev 9578)
@@ -2,23 +2,14 @@
import static org.jboss.seam.ScopeType.CONVERSATION;
-import java.io.Serializable;
-
-import org.jboss.seam.Component;
-import org.jboss.seam.ComponentType;
import org.jboss.seam.annotations.intercept.AroundInvoke;
import org.jboss.seam.annotations.intercept.Interceptor;
-import org.jboss.seam.annotations.intercept.PostActivate;
-import org.jboss.seam.annotations.intercept.PrePassivate;
-import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.BijectionInterceptor;
-import org.jboss.seam.core.Conversation;
import org.jboss.seam.intercept.AbstractInterceptor;
import org.jboss.seam.intercept.InvocationContext;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.transaction.Transaction;
-import org.jboss.seam.web.Session;
/**
* Swizzles entity references around each invocation, maintaining referential
@@ -69,8 +60,6 @@
}
}
-
-
public boolean isInterceptorEnabled()
{
return getComponent().getScope() == CONVERSATION;
Modified: trunk/src/main/org/jboss/seam/persistence/ManagedEntityWrapper.java
===================================================================
--- trunk/src/main/org/jboss/seam/persistence/ManagedEntityWrapper.java 2008-11-16
10:11:21 UTC (rev 9577)
+++ trunk/src/main/org/jboss/seam/persistence/ManagedEntityWrapper.java 2008-11-17
05:15:05 UTC (rev 9578)
@@ -6,14 +6,16 @@
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
-import java.util.List;
+import java.util.Collection;
+import java.util.Iterator;
import java.util.Map;
-import java.util.Set;
+import java.util.Map.Entry;
import org.jboss.seam.Component;
import org.jboss.seam.Seam;
import org.jboss.seam.annotations.In;
import org.jboss.seam.contexts.Contexts;
+import org.jboss.seam.core.Manager;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.util.Reflections;
@@ -22,7 +24,7 @@
* @author Gavin King
* @author Pete Muir
* @author Norman Richards
- *
+ * @author Dan Allen
*/
public class ManagedEntityWrapper
{
@@ -31,98 +33,129 @@
public void wrap(Object target, Component component) throws Exception
{
- if ( touchedContextsExist() )
+ if ( !touchedContextsExist() )
{
- Class beanClass = target.getClass();
- for (; beanClass!=Object.class; beanClass=beanClass.getSuperclass())
+ log.trace("No touched persistence contexts. Therefore, there are no
entities in this conversation whose identities need to be preserved.");
+ return;
+ }
+
+ String oldCid = switchToConversationContextOfComponent(component);
+ Class beanClass = target.getClass();
+ for (; beanClass!=Object.class; beanClass=beanClass.getSuperclass())
+ {
+ log.trace("Examining fields on " + beanClass);
+ for ( Field field: beanClass.getDeclaredFields() )
{
- log.trace("Examining fields on " + beanClass);
- for ( Field field: beanClass.getDeclaredFields() )
+ if ( !ignore(field) )
{
- if ( !ignore(field) )
+ Object value = getFieldValue(target, field);
+ if (value!=null)
{
- Object value = getFieldValue(target, field);
- if (value!=null)
+ Object dataModel = null;
+ if ( DATA_MODEL.isInstance(value) )
{
- Object dataModel = null;
- if ( DATA_MODEL.isInstance(value) )
- {
- dataModel = value;
- value = getWrappedData(dataModel);
- }
- if ( isRef(value) )
- {
- log.trace("Attempting to save wrapper for " + field +
" (" + value + ")");
- saveWrapper(target, component, field, dataModel, value);
- }
- else
- {
- log.trace("Clearing wrapper for " + field + "
(" + value + ") as it isn't a entity reference");
- clearWrapper(component, field);
- }
+ dataModel = value;
+ value = getWrappedData(dataModel);
}
+ if ( containsReferenceToEntityInstance(value) )
+ {
+ log.trace("Attempting to save wrapper for " + field +
" (" + value + ")");
+ saveWrapper(target, component, field, dataModel, value);
+ }
else
{
- log.trace("Clearing wrapper for " + field + " as it
is null");
+ log.trace("Clearing wrapper for " + field + " ("
+ value + ") as it isn't a entity reference");
clearWrapper(component, field);
}
}
else
{
- log.trace("Ignoring field " + field + " as it is static,
transient or annotated with @In");
+ log.trace("Clearing wrapper for " + field + " as it is
null");
+ clearWrapper(component, field);
}
}
+ else
+ {
+ log.trace("Ignoring field " + field + " as it is static,
transient or annotated with @In");
+ }
}
}
- else
- {
- log.trace("No touched persistence contexts");
- }
+ restorePreviousConversationContextIfNecessary(oldCid);
}
public void deserialize(Object controllerBean, Component component) throws Exception
- {
- if ( touchedContextsExist() )
+ {
+ if ( !touchedContextsExist() )
{
- Class beanClass = controllerBean.getClass();
- for (; beanClass!=Object.class; beanClass=beanClass.getSuperclass())
+ log.trace("No touched persistence contexts. Therefore, there are no
entities in this conversation whose identities need to be restored.");
+ return;
+ }
+
+ Class beanClass = controllerBean.getClass();
+ for (; beanClass!=Object.class; beanClass=beanClass.getSuperclass())
+ {
+ log.trace("Examining fields on " + beanClass);
+ for ( Field field: beanClass.getDeclaredFields() )
{
- log.trace("Examining fields on " + beanClass);
- for ( Field field: beanClass.getDeclaredFields() )
+ if ( !ignore(field) )
{
- if ( !ignore(field) )
+ Object value = getFieldValue(controllerBean, field);
+ Object dataModel = null;
+ if (value!=null && DATA_MODEL.isInstance(value) )
{
- Object value = getFieldValue(controllerBean, field);
- Object dataModel = null;
- if (value!=null && DATA_MODEL.isInstance(value) )
- {
- dataModel = value;
- }
- log.trace("Attempting to restore wrapper for " + field +
" (" + value + ")");
- //TODO: be more selective
- getFromWrapper(controllerBean, component, field, dataModel);
+ dataModel = value;
}
- else
- {
- log.trace("Ignoring field " + field + " as it is static,
transient or annotated with @In");
- }
+ log.trace("Attempting to restore wrapper for " + field + "
(" + value + ")");
+ //TODO: be more selective
+ getFromWrapper(controllerBean, component, field, dataModel);
}
+ else
+ {
+ log.trace("Ignoring field " + field + " as it is static,
transient or annotated with @In");
+ }
}
}
- else
- {
- log.trace("No touched persistence contexts");
- }
}
- private boolean isRef(Object value)
+ private boolean containsReferenceToEntityInstance(Object value)
{
- //TODO: could do better by checking if the
- // collection really contains an entity
- return value instanceof List ||
- value instanceof Map ||
- value instanceof Set ||
- Seam.getEntityClass(value.getClass()) != null;
+ if (value == null)
+ {
+ return false;
+ }
+ else if (value instanceof Collection)
+ {
+ // Do a lazy man's generic check by scanning the collection until an entity
is found (nested objects not considered).
+ for (Iterator iter = ((Collection) value).iterator(); iter.hasNext();)
+ {
+ Object v = iter.next();
+ if (v != null && Seam.getEntityClass(v.getClass()) != null)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ else if (value instanceof Map)
+ {
+ // Do a lazy man's generic check by scanning the collection until an entity
is found (nested objects not considered).
+ for (Iterator iter = ((Map) value).entrySet().iterator(); iter.hasNext();)
+ {
+ Entry e = (Entry) iter.next();
+ if ((e.getKey() != null && Seam.getEntityClass(e.getKey().getClass())
!= null) ||
+ (e.getValue() != null &&
Seam.getEntityClass(e.getValue().getClass()) != null))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ else if (Seam.getEntityClass(value.getClass()) != null)
+ {
+ return true;
+ }
+
+ return false;
}
private Object getFieldValue(Object bean, Field field) throws Exception
@@ -183,6 +216,37 @@
}
}
}
-
+ /**
+ * Changes the thread's current conversation context to the one that holds a
reference to this
+ * component. This is necessary if a nested conversation is making a call to a
component in
+ * a parent conversation.
+ */
+ private String switchToConversationContextOfComponent(Component component)
+ {
+ Manager manager = Manager.instance();
+ if (manager.isNestedConversation())
+ {
+ String currentCid = manager.getCurrentConversationId();
+ String residentCid =
manager.getCurrentConversationEntry().findPositionInConversationStack(component);
+ if (residentCid != currentCid)
+ {
+ Contexts.getConversationContext().flush();
+ Manager.instance().switchConversation(residentCid);
+ return currentCid;
+ }
+ }
+
+ return null;
+ }
+
+ private void restorePreviousConversationContextIfNecessary(String oldCid)
+ {
+ if (oldCid != null)
+ {
+ Contexts.getConversationContext().flush();
+ Manager.instance().switchConversation(oldCid);
+ }
+ }
+
}
Modified:
trunk/src/test/integration/src/org/jboss/seam/test/integration/EntityPassivationTest.java
===================================================================
---
trunk/src/test/integration/src/org/jboss/seam/test/integration/EntityPassivationTest.java 2008-11-16
10:11:21 UTC (rev 9577)
+++
trunk/src/test/integration/src/org/jboss/seam/test/integration/EntityPassivationTest.java 2008-11-17
05:15:05 UTC (rev 9578)
@@ -1,6 +1,7 @@
package org.jboss.seam.test.integration;
import java.io.Serializable;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -9,135 +10,243 @@
import javax.persistence.EntityManager;
+import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.AutoCreate;
+import org.jboss.seam.annotations.Begin;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
+import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Conversation;
import org.jboss.seam.mock.SeamTest;
+import org.jboss.seam.persistence.ManagedEntityInterceptor;
import org.testng.annotations.Test;
-public class EntityPassivationTest
- extends SeamTest
+/**
+ * Verifies the work of the ManagedEntityInterceptor. Specifically that
+ * collections containing entity instances are properly put into the session and
+ * pulled back into the conversation-scoped component between requests (during a
+ * potential period of passivation of the SFSB). The test also verifies that
+ * when a component in a nested conversation calls a component in a parent
+ * conversation, that the passivated state is kept with the parent conversation.
+ *
+ * @author Norman Richards
+ * @author Dan Allen
+ */
+public class EntityPassivationTest extends SeamTest
{
+ @Test
+ public void testEntityList() throws Exception
+ {
+ String pid = new FacesRequest("/test.xhtml")
+ {
+ @Override
+ protected void invokeApplication() throws Exception
+ {
+ // MEI is not installed by default, so we need to enable it
+ Component.forName("entitytest.someComponent").addInterceptor(new
ManagedEntityInterceptor());
+ Component.forName("entitytest.nestedComponent").addInterceptor(new
ManagedEntityInterceptor());
+
+ Conversation.instance().begin(true, false);
- @Test
- public void testEntityList()
- throws Exception
- {
- String id = new FacesRequest("/test.xhtml") {
- @Override
- protected void invokeApplication()
- throws Exception
- {
- Conversation.instance().begin(true, false);
+ invokeAction("#{entitytest.someComponent.createSomeThings}");
+ invokeAction("#{entitytest.someComponent.loadThings}");
+ }
- invokeAction("#{entitytest.someComponent.createSomeThings}");
- invokeAction("#{entitytest.someComponent.loadThings}");
+ @Override
+ protected void renderResponse() throws Exception
+ {
+ Object thing = getValue("#{entitytest.someComponent.thing}");
+ assert thing != null;
- }
- @Override
- protected void renderResponse()
- throws Exception
- {
- Object thing = getValue("#{entitytest.someComponent.thing}");
- assert thing!=null;
-
- List thingList = (List)
getValue("#{entitytest.someComponent.thingsAsList}");
- assert thingList!=null && !thingList.isEmpty();
- assert thingList.get(0) != null;
-
-
- Set thingSet = (Set)
getValue("#{entitytest.someComponent.thingsAsSet}");
- assert thingSet!=null && thingSet.size() > 0;
- assert thingSet.iterator().next() != null;
-
- Map thingMap = (Map)
getValue("#{entitytest.someComponent.thingsAsMap}");
- assert thingMap!=null && thingMap.size() > 0;
- }
- }.run();
+ List thingList = (List)
getValue("#{entitytest.someComponent.thingsAsList}");
+ assert thingList != null && !thingList.isEmpty();
+ assert thingList.get(0) != null;
- new FacesRequest("/test.xhtml", id) {
- // the entities should be passivated
- }.run();
+ Set thingSet = (Set)
getValue("#{entitytest.someComponent.thingsAsSet}");
+ assert thingSet != null && thingSet.size() > 0;
+ assert thingSet.iterator().next() != null;
- new FacesRequest("/test.xhtml", id) {
- // passivated a second time
- }.run();
+ Map thingMap = (Map)
getValue("#{entitytest.someComponent.thingsAsMap}");
+ assert thingMap != null && thingMap.size() > 0;
+ }
+ }.run();
- new FacesRequest("/test.xhtml", id) {
- @Override
- protected void renderResponse()
- throws Exception
- {
- Object thing = getValue("#{entitytest.someComponent.thing}");
- assert thing!=null;
-
- List thingList = (List)
getValue("#{entitytest.someComponent.thingsAsList}");
- assert thingList!=null && !thingList.isEmpty();
- assert thingList.get(0) != null;
-
-
- Set thingSet = (Set)
getValue("#{entitytest.someComponent.thingsAsSet}");
- assert thingSet!=null && thingSet.size() > 0;
- assert thingSet.iterator().next() != null;
-
- Map thingMap = (Map)
getValue("#{entitytest.someComponent.thingsAsMap}");
- assert thingMap!=null && thingMap.size() > 0;
- }
-
-
- }.run();
- }
-
-
-
- @Name("entitytest.someComponent")
- @Scope(ScopeType.CONVERSATION)
- @AutoCreate
- public static class SomeComponent implements Serializable {
- @In EntityManager entityManager;
-
-
- Set<UnversionedThing> thingSet;
- List<UnversionedThing> thingList;
- Map<Long,UnversionedThing> thingMap;
- UnversionedThing thing;
-
- public void loadThings() {
- thingList = entityManager.createQuery("select t from UnversionedThing
t").getResultList();
- thingSet = new HashSet<UnversionedThing>(thingList);
-
- thingMap = new HashMap<Long,UnversionedThing>();
- for (UnversionedThing thing: thingList) {
- thingMap.put(thing.getId(),thing);
- }
-
- thing = thingList.get(0);
- }
-
- public List<UnversionedThing> getThingsAsList() {
- return thingList;
- }
-
- public Set<UnversionedThing> getThingsAsSet() {
- return thingSet;
- }
-
- public Map<Long,UnversionedThing> getThingsAsMap() {
- return thingMap;
- }
-
- public UnversionedThing getThing() {
- return thing;
- }
-
- public void createSomeThings() {
- UnversionedThing thing1 = new UnversionedThing();
- thing1.setName("thing one");
- entityManager.persist(thing1);
- }
-
- }
+ new FacesRequest("/test.xhtml", pid)
+ {
+ // the entities should be passivated
+ }.run();
+
+ new FacesRequest("/test.xhtml", pid)
+ {
+ // passivated a second time
+ }.run();
+
+ new FacesRequest("/test.xhtml", pid)
+ {
+ @Override
+ protected void renderResponse() throws Exception
+ {
+ Object thing = getValue("#{entitytest.someComponent.thing}");
+ assert thing != null;
+
+ List thingList = (List)
getValue("#{entitytest.someComponent.thingsAsList}");
+ assert thingList != null && !thingList.isEmpty();
+ assert thingList.get(0) != null;
+
+ Set thingSet = (Set)
getValue("#{entitytest.someComponent.thingsAsSet}");
+ assert thingSet != null && thingSet.size() > 0;
+ assert thingSet.iterator().next() != null;
+
+ Map thingMap = (Map)
getValue("#{entitytest.someComponent.thingsAsMap}");
+ assert thingMap != null && thingMap.size() > 0;
+ }
+
+ }.run();
+
+ // Start a nested conversation to verify that calls to a component in a parent
conversation
+ // will passivate that component's state in the parent conversation context and
not in
+ // the nested conversation. Thus, when the parent conversation is restored, its
state
+ // will be properly restored.
+ String nid = new FacesRequest("/test.xhtml", pid)
+ {
+ @Override
+ protected void invokeApplication() throws Exception
+ {
+ invokeMethod("#{entitytest.nestedComponent.nest}");
+ }
+
+ @Override
+ protected void renderResponse() throws Exception
+ {
+ assert Conversation.instance().isNested();
+ }
+
+ }.run();
+
+ new FacesRequest("/test.xhtml", nid)
+ {
+ @Override
+ protected void invokeApplication() throws Exception
+ {
+ // invoke component in parent conversation from nested conversation
+
invokeMethod("#{entitytest.someComponent.removeFirstThingFromList}");
+ }
+
+ @Override
+ protected void renderResponse() throws Exception
+ {
+ // the nested conversation should not hold the serialized property of the
component in the parent conversation
+ assert
!Arrays.asList(Contexts.getConversationContext().getNames()).contains("entitytest.someComponent.thingList");
+ List thingList = (List)
getValue("#{entitytest.someComponent.thingsAsList}");
+ assert thingList.size() == 1;
+ }
+ }.run();
+
+ new FacesRequest("/test.xhtml", nid)
+ {
+ @Override
+ protected void invokeApplication() throws Exception
+ {
+ invokeMethod("#{entitytest.nestedComponent.end}");
+ }
+ }.run();
+
+ new FacesRequest("/test.xhtml", pid)
+ {
+ @Override
+ protected void renderResponse() throws Exception
+ {
+ // The state of the component in the parent conversation should be
preserved.
+ List thingList = (List)
getValue("#{entitytest.someComponent.thingsAsList}");
+ assert thingList.size() == 1;
+ }
+ }.run();
+ }
+
+ @Name("entitytest.someComponent")
+ @Scope(ScopeType.CONVERSATION)
+ @AutoCreate
+ public static class SomeComponent implements Serializable
+ {
+ @In
+ EntityManager entityManager;
+
+ Set<UnversionedThing> thingSet;
+ List<UnversionedThing> thingList;
+ Map<Long, UnversionedThing> thingMap;
+ UnversionedThing thing;
+
+ public void loadThings()
+ {
+ thingList = entityManager.createQuery("select t from UnversionedThing
t").getResultList();
+ thingSet = new HashSet<UnversionedThing>(thingList);
+
+ thingMap = new HashMap<Long, UnversionedThing>();
+ for (UnversionedThing thing : thingList)
+ {
+ thingMap.put(thing.getId(), thing);
+ }
+
+ thing = thingList.get(0);
+ }
+
+ public List<UnversionedThing> getThingsAsList()
+ {
+ return thingList;
+ }
+
+ public Set<UnversionedThing> getThingsAsSet()
+ {
+ return thingSet;
+ }
+
+ public Map<Long, UnversionedThing> getThingsAsMap()
+ {
+ return thingMap;
+ }
+
+ public UnversionedThing getThing()
+ {
+ return thing;
+ }
+
+ public void removeFirstThingFromList()
+ {
+ loadThings();
+ thingList.remove(0);
+ }
+
+ public void createSomeThings()
+ {
+ UnversionedThing thing1 = new UnversionedThing();
+ thing1.setName("thing one");
+ entityManager.persist(thing1);
+
+ UnversionedThing thing2 = new UnversionedThing();
+ thing1.setName("thing two");
+ entityManager.persist(thing2);
+ }
+
+ }
+
+ @Name("entitytest.nestedComponent")
+ @Scope(ScopeType.CONVERSATION)
+ @AutoCreate
+ public static class NestedComponent implements Serializable
+ {
+ @In
+ EntityManager entityManager;
+
+ @Begin(nested = true)
+ public void nest()
+ {
+ }
+
+ public void end()
+ {
+ Conversation.instance().end();
+ }
+ }
}