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