View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one
    3    * or more contributor license agreements.  See the NOTICE file
    4    * distributed with this work for additional information
    5    * regarding copyright ownership.  The ASF licenses this file
    6    * to you under the Apache License, Version 2.0 (the
    7    * "License"); you may not use this file except in compliance
    8    * with the License.  You may obtain a copy of the License at
    9    *
    10   *   http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing,
    13   * software distributed under the License is distributed on an
    14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   * KIND, either express or implied.  See the License for the
    16   * specific language governing permissions and limitations
    17   * under the License.
    18   */
    19
    20  package org.apache.myfaces.orchestra.conversation;
    21
    22  import java.io.IOException;
    23  import java.io.ObjectStreamException;
    24  import java.io.Serializable;
    25  import java.util.Collections;
    26  import java.util.HashMap;
    27  import java.util.Iterator;
    28  import java.util.Map;
    29
    30  import org.apache.commons.logging.Log;
    31  import org.apache.commons.logging.LogFactory;
    32  import org.apache.myfaces.orchestra.FactoryFinder;
    33  import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
    34  import org.apache.myfaces.orchestra.lib.OrchestraException;
    35  import org.apache.myfaces.orchestra.requestParameterProvider.RequestParameterProviderManager;
    36
    37  /**
    38   * Deals with the various conversation contexts in the current session.
    39   * <p>
    40   * There is expected to be one instance of this class per http-session, managing all of the
    41   * data associated with all browser windows that use that http-session.
    42   * <p>
    43   * One particular task of this class is to return "the current" ConversationContext object for
    44   * the current http request (from the set of ConversationContext objects that this manager
    45   * object holds). The request url is presumed to include a query-parameter that specifies the
    46   * id of the appropriate ConversationContext object to be used. If no such query-parameter is
    47   * present, then a new ConversationContext object will automatically be created.
    48   * <p>
    49   * At the current time, this object does not serialize well. Any attempt to serialize
    50   * this object (including any serialization of the user session) will just cause it
    51   * to be discarded.
    52   * <p>
    53   * TODO: fix serialization issues.
    54   */
    55  public class ConversationManager implements Serializable
    56  {
    57      private static final long serialVersionUID = 1L;
    58
    59      final static String CONVERSATION_CONTEXT_PARAM = "conversationContext";
    60
    61      private final static String CONVERSATION_MANAGER_KEY = "org.apache.myfaces.ConversationManager";
    62      private final static String CONVERSATION_CONTEXT_REQ = "org.apache.myfaces.ConversationManager.conversationContext";
    63
    64      private static final Iterator EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();
    65
    66      // See method readResolve
    67      private static final Object DUMMY = new Integer(-1);
    68
    69      private final Log log = LogFactory.getLog(ConversationManager.class);
    70
    71      /**
    72       * Used to generate a unique id for each "window" that a user has open
    73       * on the same webapp within the same HttpSession. Note that this is a
    74       * property of an object stored in the session, so will correctly
    75       * migrate from machine to machine along with a distributed HttpSession.
    76       *
    77       */
    78      private long nextConversationContextId = 1;
    79
    80      // This member must always be accessed with a lock held on the parent ConverstationManager instance;
    81      // a HashMap is not thread-safe and this class must be thread-safe.
    82      private final Map conversationContexts = new HashMap();
    83
    84      protected ConversationManager()
    85      {
    86      }
    87
    88      /**
    89       * Get the conversation manager for the current http session.
    90       * <p>
    91       * If none exists, then a new instance is allocated and stored in the current http session.
    92       * Null is never returned.
    93       * <p>
    94       * Throws IllegalStateException if the Orchestra FrameworkAdapter has not been correctly
    95       * configured.
    96       */
    97      public static ConversationManager getInstance()
    98      {
    99          return getInstance(true);
    100     }
    101
    102     /**
    103      * Get the conversation manager for the current http session.
    104      * <p>
    105      * When create is true, an instance is always returned; one is created if none currently exists
    106      * for the current user session.
    107      * <p>
    108      * When create is false, null is returned if no instance yet exists for the current user session.
    109      */
    110     public static ConversationManager getInstance(boolean create)
    111     {
    112         FrameworkAdapter frameworkAdapter = FrameworkAdapter.getCurrentInstance();
    113         if (frameworkAdapter == null)
    114         {
    115             if (!create)
    116             {
    117                 // if we don't have to create a conversation manager, then it doesn't
    118                 // matter if there is no FrameworkAdapter available.
    119                 return null;
    120             }
    121             else
    122             {
    123                 throw new IllegalStateException("FrameworkAdapter not found");
    124             }
    125         }
    126
    127         Object cmObj = frameworkAdapter.getSessionAttribute(CONVERSATION_MANAGER_KEY);
    128         // hack: see method readResolve
    129         if (cmObj == DUMMY)
    130         {
    131             Log log = LogFactory.getLog(ConversationManager.class);
    132             log.debug("Method getInstance found dummy ConversationManager object");
    133             cmObj = null;
    134         }
    135
    136
    137         ConversationManager conversationManager = (ConversationManager) cmObj;
    138
    139         if (conversationManager == null && create)
    140         {
    141             Log log = LogFactory.getLog(ConversationManager.class);
    142             log.debug("Register ConversationRequestParameterProvider");
    143
    144             conversationManager = FactoryFinder.getConversationManagerFactory().createConversationManager();
    145
    146             // initialize environmental systems
    147             RequestParameterProviderManager.getInstance().register(new ConversationRequestParameterProvider());
    148
    149             // set mark
    150             FrameworkAdapter.getCurrentInstance().setSessionAttribute(CONVERSATION_MANAGER_KEY, conversationManager);
    151         }
    152
    153         return conversationManager;
    154     }
    155
    156     /**
    157      * Get the current conversationContextId.
    158      * <p>
    159      * If there is no current conversationContext, then null is returned.
    160      */
    161     private Long findConversationContextId()
    162     {
    163         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
    164
    165         // Has it been extracted from the req params and cached as a req attr?
    166         Long conversationContextId = (Long)fa.getRequestAttribute(CONVERSATION_CONTEXT_REQ);
    167         if (conversationContextId == null)
    168         {
    169             if (fa.containsRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM))
    170             {
    171                 String urlConversationContextId = fa.getRequestParameterAttribute(
    172                         CONVERSATION_CONTEXT_PARAM).toString();
    173                 conversationContextId = new Long(
    174                         Long.parseLong(urlConversationContextId, Character.MAX_RADIX));
    175             }
    176         }
    177         return conversationContextId;
    178     }
    179
    180     /**
    181      * Get the current, or create a new unique conversationContextId.
    182      * <p>
    183      * The current conversationContextId will be retrieved from the request
    184      * parameters. If no such parameter is present then a new id will be
    185      * allocated <i>and configured as the current conversation id</i>.
    186      * <p>
    187      * In either case the result will be stored within the request for
    188      * faster lookup.
    189      * <p>
    190      * Note that there is no security flaw regarding injection of fake
    191      * context ids; the id must match one already in the session and there
    192      * is no security problem with two windows in the same session exchanging
    193      * ids.
    194      * <p>
    195      * This method <i>never</i> returns null.
    196      */
    197     private Long getOrCreateConversationContextId()
    198     {
    199         Long conversationContextId = findConversationContextId();
    200         if (conversationContextId == null)
    201         {
    202             conversationContextId = createNextConversationContextId();
    203             FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
    204             fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, conversationContextId);
    205         }
    206
    207         return conversationContextId;
    208     }
    209
    210     /**
    211      * Get the current, or create a new unique conversationContextId.
    212      * <p>
    213      * This method is deprecated because, unlike all the other get methods, it
    214      * actually creates the value if it does not exist. Other get methods (except
    215      * getInstance) return null if the data does not exist. In addition, this
    216      * method is not really useful to external code and probably should never
    217      * have been exposed as a public API in the first place; external code should
    218      * never need to force the creation of a ConversationContext.
    219      * <p>
    220      * For internal use within this class, use either findConversationContextId()
    221      * or getOrCreateConversationContextId().
    222      * <p>
    223      * To just obtain the current ConversationContext <i>if it exists</i>, see
    224      * method getCurrentConversationContext().
    225      *
    226      * @deprecated This method should not be needed by external classes, and
    227      * was inconsistent with other methods on this class.
    228      */
    229     public Long getConversationContextId()
    230     {
    231         return getOrCreateConversationContextId();
    232     }
    233
    234     /**
    235      * Allocate a new Long value for use as a conversation context id.
    236      * <p>
    237      * The returned value must not match any conversation context id already in
    238      * use within this ConversationManager instance (which is scoped to the
    239      * current http session).
    240      */
    241     private Long createNextConversationContextId()
    242     {
    243         Long conversationContextId;
    244         synchronized(this)
    245         {
    246             conversationContextId = new Long(nextConversationContextId);
    247             nextConversationContextId++;
    248         }
    249         return conversationContextId;
    250     }
    251
    252     /**
    253      * Get the conversation context for the given id.
    254      * <p>
    255      * Null is returned if there is no ConversationContext with the specified id.
    256      * <p>
    257      * Param conversationContextId must not be null.
    258      * <p>
    259      * Public since version 1.3.
    260      */
    261     public ConversationContext getConversationContext(Long conversationContextId)
    262     {
    263         synchronized (this)
    264         {
    265             return (ConversationContext) conversationContexts.get(conversationContextId);
    266         }
    267     }
    268
    269     /**
    270      * Get the conversation context for the given id.
    271      * <p>
    272      * If there is no such conversation context a new one will be created.
    273      * The new conversation context will be a "top-level" context (ie has no parent).
    274      * <p>
    275      * The new conversation context will <i>not</i> be the current conversation context,
    276      * unless the id passed in was already configured as the current conversation context id.
    277      */
    278     protected ConversationContext getOrCreateConversationContext(Long conversationContextId)
    279     {
    280         synchronized (this)
    281         {
    282             ConversationContext conversationContext = (ConversationContext) conversationContexts.get(
    283                     conversationContextId);
    284             if (conversationContext == null)
    285             {
    286                 ConversationContextFactory factory = FactoryFinder.getConversationContextFactory();
    287                 conversationContext = factory.createConversationContext(null, conversationContextId.longValue());
    288                 conversationContexts.put(conversationContextId, conversationContext);
    289
    290                 // TODO: add the "user" name here, otherwise this debugging is not very useful
    291                 // except when testing a webapp with only one user.
    292                 log.debug("Created context " + conversationContextId);
    293             }
    294             return conversationContext;
    295         }
    296     }
    297
    298     /**
    299      * This will create a new conversation context using the specified context as
    300      * its parent.
    301      * <p>
    302      * The returned context is not selected as the "current" one; see activateConversationContext.
    303      *
    304      * @since 1.3
    305      */
    306     public ConversationContext createConversationContext(ConversationContext parent)
    307     {
    308         Long ctxId = createNextConversationContextId();
    309         ConversationContextFactory factory = FactoryFinder.getConversationContextFactory();
    310         ConversationContext ctx = factory.createConversationContext(parent, ctxId.longValue());
    311
    312         synchronized(this)
    313         {
    314             conversationContexts.put(ctxId, ctx);
    315         }
    316
    317         return ctx;
    318     }
    319
    320     /**
    321      * Make the specific context the current context for the current HTTP session.
    322      * <p>
    323      * Methods like getCurrentConversationContext will then return the specified
    324      * context object.
    325      *
    326      * @since 1.2
    327      */
    328     public void activateConversationContext(ConversationContext ctx)
    329     {
    330         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
    331         fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, ctx.getIdAsLong());
    332     }
    333
    334     /**
    335      * Ends all conversations within the current context; the context itself will remain active.
    336      */
    337     public void clearCurrentConversationContext()
    338     {
    339         Long conversationContextId = findConversationContextId();
    340         if (conversationContextId != null)
    341         {
    342             ConversationContext conversationContext = getConversationContext(conversationContextId);
    343             if (conversationContext != null)
    344             {
    345                 conversationContext.invalidate();
    346             }
    347         }
    348     }
    349
    350     /**
    351      * Removes the specified contextId from the set of known contexts,
    352      * and deletes every conversation in it.
    353      * <p>
    354      * Objects in the conversation which implement ConversationAware
    355      * will have callbacks invoked.
    356      * <p>
    357      * The conversation being removed must not be the currently active
    358      * context. If it is, then method activateConversationContext should
    359      * first be called on some other instance (perhaps the parent of the
    360      * one being removed) before this method is called.
    361      *
    362      * @since 1.3
    363      */
    364     public void removeAndInvalidateConversationContext(ConversationContext context)
    365     {
    366         if (context.hasChildren())
    367         {
    368             throw new OrchestraException("Cannot remove context with children");
    369         }
    370
    371         if (context.getIdAsLong().equals(findConversationContextId()))
    372         {
    373             throw new OrchestraException("Cannot remove current context");
    374         }
    375
    376         synchronized(conversationContexts)
    377         {
    378             conversationContexts.remove(context.getIdAsLong());
    379         }
    380
    381         ConversationContext parent = context.getParent();
    382         if (parent != null)
    383         {
    384             parent.removeChild(context);
    385         }
    386
    387         context.invalidate();
    388
    389         // TODO: add the deleted context ids to a list stored in the session,
    390         // and redirect to an error page if any future request specifies this id.
    391         // This catches things like going "back" into a flow that has ended, or
    392         // navigating with the parent page of a popup flow (which kills the popup
    393         // flow context) then trying to use the popup page.
    394         //
    395         // We cannot simply report an error for every case where an invalid id is
    396         // used, because bookmarks will have ids in them; when the bookmark is used
    397         // after the session has died we still want the bookmark url to work. Possibly
    398         // we should allow GET with a bad id, but always fail a POST with one?
    399     }
    400
    401     /**
    402      * Removes the specified contextId from the set of known contexts.
    403      * <p>
    404      * It does nothing else. Maybe it should be called "detachConversationContext"
    405      * or similar.
    406      *
    407      * @deprecated This method is not actually used by anything.
    408      */
    409     protected void removeConversationContext(Long conversationContextId)
    410     {
    411         synchronized (this)
    412         {
    413             conversationContexts.remove(conversationContextId);
    414         }
    415     }
    416
    417     /**
    418      * Start a conversation.
    419      *
    420      * @see ConversationContext#startConversation(String, ConversationFactory)
    421      */
    422     public Conversation startConversation(String name, ConversationFactory factory)
    423     {
    424         ConversationContext conversationContext = getOrCreateCurrentConversationContext();
    425         return conversationContext.startConversation(name, factory);
    426     }
    427
    428     /**
    429      * Remove a conversation
    430      *
    431      * Note: It is assumed that the conversation has already been invalidated
    432      *
    433      * @see ConversationContext#removeConversation(String)
    434      */
    435     protected void removeConversation(String name)
    436     {
    437         Long conversationContextId = findConversationContextId();
    438         if (conversationContextId != null)
    439         {
    440             ConversationContext conversationContext = getConversationContext(conversationContextId);
    441             if (conversationContext != null)
    442             {
    443                 conversationContext.removeConversation(name);
    444             }
    445         }
    446     }
    447
    448     /**
    449      * Get the conversation with the given name
    450      *
    451      * @return null if no conversation context is active or if the conversation did not exist.
    452      */
    453     public Conversation getConversation(String name)
    454     {
    455         ConversationContext conversationContext = getCurrentConversationContext();
    456         if (conversationContext == null)
    457         {
    458             return null;
    459         }
    460         return conversationContext.getConversation(name);
    461     }
    462
    463     /**
    464      * check if the given conversation is active
    465      */
    466     public boolean hasConversation(String name)
    467     {
    468         ConversationContext conversationContext = getCurrentConversationContext();
    469         if (conversationContext == null)
    470         {
    471             return false;
    472         }
    473         return conversationContext.hasConversation(name);
    474     }
    475
    476     /**
    477      * Returns an iterator over all the Conversation objects in the current conversation
    478      * context. Never returns null, even if no conversation context exists.
    479      */
    480     public Iterator iterateConversations()
    481     {
    482         ConversationContext conversationContext = getCurrentConversationContext();
    483         if (conversationContext == null)
    484         {
    485             return EMPTY_ITERATOR;
    486         }
    487
    488         return conversationContext.iterateConversations();
    489     }
    490
    491     /**
    492      * Get the current conversation context.
    493      * <p>
    494      * In a simple Orchestra application this will always be a root conversation context.
    495      * When using a dialog/page-flow environment the context that is returned might have
    496      * a parent context.
    497      * <p>
    498      * Null is returned if there is no current conversationContext.
    499      */
    500     public ConversationContext getCurrentConversationContext()
    501     {
    502         Long ccid = findConversationContextId();
    503         if (ccid == null)
    504         {
    505             return null;
    506         }
    507         else
    508         {
    509             ConversationContext ctx = getConversationContext(ccid);
    510             if (ctx == null)
    511             {
    512                 // Someone has perhaps used the back button to go back into a context
    513                 // that has already ended. This simply will not work, so we should
    514                 // throw an exception here.
    515                 //
    516                 // Or somebody might have just activated a bookmark. Unfortunately,
    517                 // when someone bookmarks a page within an Orchestra app, the bookmark
    518                 // will capture the contextId too.
    519                 //
    520                 // There is unfortunately no obvious way to tell these two actions apart.
    521                 // So we cannot report an error here; instead, just return a null context
    522                 // so that a new instance gets created - and hope that the page itself
    523                 // detects the problem and reports an error if it needs conversation state
    524                 // that does not exist.
    525                 //
    526                 // What we should do here *at least* is bump the nextConversationId value
    527                 // to be greater than this value, so that we don't later try to allocate a
    528                 // second conversation with the same id. Yes, evil users could pass a very
    529                 // high value here and cause wraparound but that is really not a problem as
    530                 // they can only screw themselves up.
    531                 log.warn("ConversationContextId specified but context does not exist");
    532                 synchronized(this)
    533                 {
    534                     if (nextConversationContextId <= ccid.longValue())
    535                     {
    536                         nextConversationContextId = ccid.longValue() + 1;
    537                     }
    538                 }
    539                 return null;
    540             }
    541             return ctx;
    542         }
    543     }
    544
    545     /**
    546      * Return the current ConversationContext for the current http session;
    547      * if none yet exists then a ConversationContext is created and configured
    548      * as the current context.
    549      * <p>
    550      * This is currently package-scoped because it is not clear that code
    551      * outside orchestra can have any use for this method. The only user
    552      * outside of this class is ConversationRequestParameterProvider.
    553      *
    554      * @since 1.2
    555      */
    556     ConversationContext getOrCreateCurrentConversationContext()
    557     {
    558         Long ccid = getOrCreateConversationContextId();
    559         return getOrCreateConversationContext(ccid);
    560     }
    561
    562     /**
    563      * Return true if there is a conversation context associated with the
    564      * current request.
    565      */
    566     public boolean hasConversationContext()
    567     {
    568         return getCurrentConversationContext() == null;
    569     }
    570
    571     /**
    572      * Get the current root conversation context (aka the window conversation context).
    573      * <p>
    574      * Null is returned if it does not exist.
    575      *
    576      * @since 1.2
    577      */
    578     public ConversationContext getCurrentRootConversationContext()
    579     {
    580         Long ccid = findConversationContextId();
    581         if (ccid == null)
    582         {
    583             return null;
    584         }
    585
    586         synchronized (this)
    587         {
    588             ConversationContext conversationContext = getConversationContext(ccid);
    589             if (conversationContext == null)
    590             {
    591                 return null;
    592             }
    593             else
    594             {
    595                 return conversationContext.getRoot();
    596             }
    597         }
    598     }
    599
    600     /**
    601      * Get the Messager used to inform the user about anomalies.
    602      * <p>
    603      * What instance is returned is controlled by the FrameworkAdapter. See
    604      * {@link org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter} for details.
    605      */
    606     public ConversationMessager getMessager()
    607     {
    608         return FrameworkAdapter.getCurrentInstance().getConversationMessager();
    609     }
    610
    611     /**
    612      * Check the timeout for each conversation context, and all conversations
    613      * within those contexts.
    614      * <p>
    615      * If any conversation has not been accessed within its timeout period
    616      * then clear the context.
    617      * <p>
    618      * Invoke the checkTimeout method on each context so that any conversation
    619      * that has not been accessed within its timeout is invalidated.
    620      */
    621     protected void checkTimeouts()
    622     {
    623         Map.Entry[] contexts;
    624         synchronized (this)
    625         {
    626             contexts = new Map.Entry[conversationContexts.size()];
    627             conversationContexts.entrySet().toArray(contexts);
    628         }
    629
    630         long checkTime = System.currentTimeMillis();
    631
    632         for (int i = 0; i<contexts.length; i++)
    633         {
    634             Map.Entry context = contexts[i];
    635
    636             ConversationContext conversationContext = (ConversationContext) context.getValue();
    637             if (conversationContext.hasChildren())
    638             {
    639                 // Never time out contexts that have children. Let the children time out first...
    640                 continue;
    641             }
    642
    643             conversationContext.checkConversationTimeout();
    644
    645             if (conversationContext.getTimeout() > -1 &&
    646                 (conversationContext.getLastAccess() +
    647                 conversationContext.getTimeout()) < checkTime)
    648             {
    649                 if (log.isDebugEnabled())
    650                 {
    651                     log.debug("end conversation context due to timeout: " + conversationContext.getId());
    652                 }
    653
    654                 removeAndInvalidateConversationContext(conversationContext);
    655             }
    656         }
    657     }
    658
    659     /**
    660      * @since 1.4
    661      */
    662     public void removeAndInvalidateAllConversationContexts()
    663     {
    664         ConversationContext[] contexts;
    665         synchronized (this)
    666         {
    667             contexts = new ConversationContext[conversationContexts.size()];
    668             conversationContexts.values().toArray(contexts);
    669         }
    670
    671         for (int i = 0; i<contexts.length; i++)
    672         {
    673             ConversationContext context = contexts[i];
    674             removeAndInvalidateConversationContextAndChildren(context);
    675         }
    676     }
    677
    678     private void removeAndInvalidateConversationContextAndChildren(ConversationContext conversationContext)
    679     {
    680         while (conversationContext.hasChildren())
    681         {
    682             // Get first child
    683             ConversationContext child = (ConversationContext) conversationContext.getChildren().iterator().next();
    684
    685             // This call removes child from conversationContext.children
    686             removeAndInvalidateConversationContextAndChildren(child);
    687         }
    688
    689         if (log.isDebugEnabled())
    690         {
    691             log.debug("end conversation context: " + conversationContext.getId());
    692         }
    693
    694         removeAndInvalidateConversationContext(conversationContext);
    695     }
    696
    697     private void writeObject(java.io.ObjectOutputStream out) throws IOException
    698     {
    699         // the conversation manager is not (yet) serializable, we just implement it
    700         // to make it work with distributed sessions
    701     }
    702
    703     private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
    704     {
    705         // nothing written, so nothing to read
    706     }
    707
    708     private Object readResolve() throws ObjectStreamException
    709     {
    710         // Note: Returning null here is not a good idea (for Tomcat 6.0.16 at least). Null objects are
    711         // not permitted within an HttpSession; calling HttpSession.setAttribute(name, null) is defined as
    712         // removing the attribute. So returning null here when deserializing an object from the session
    713         // can cause problems.
    714         //
    715         // Note that nothing should have a reference to the ConversationManager *except* the entry
    716         // in the http session; all other code should look it up "on demand" via the getInstance
    717         // method rather than storing a reference to it. So we can do pretty much anything we like
    718         // here as long as the getInstance() method works correctly later. Thus:
    719         //  * returning null here is one option (getInstance just creates the item later) - except
    720         //    that tomcat doesn't like it.
    721         // * creating a new object instance that getInstance will later simply find and return will
    722         //   work - except that the actual type to create can be overridden via the dependency-injection
    723         //   config, and the FrameworkAdapter class that gives us access to that info is not available
    724         //   at the current time.
    725         //
    726         // To solve this, we use a hack: a special DUMMY object is returned (and therefore will be inserted
    727         // into the HTTP session under the ConversationManager key). The getInstance method then checks
    728         // for this dummy object, and treats it like NULL. Conveniently, it appears that the serialization
    729         // mechanism doesn't care if readResolve returns an object that is not a subclass of the one that
    730         // is being deserialized, so here we can return any old object (eg an Integer).
    731         //
    732         // An alternative would be to just remove the ConversationManager object from the http session
    733         // on passivate, so that this readResolve method is never called. However hopefully at some
    734         // future time we *will* get serialization for this class working nicely and then will need
    735         // to discard these serialization hacks; it is easier to do that when the hacks are all in
    736         // the same class.
    737
    738         Log log = LogFactory.getLog(ConversationManager.class);
    739         log.debug("readResolve returning dummy ConversationManager object");
    740         return DUMMY;
    741     }
    742 }