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 org.apache.commons.logging.Log;
    23  import org.apache.commons.logging.LogFactory;
    24
    25  import java.util.Map;
    26  import java.util.TreeMap;
    27
    28  /**
    29   * A Conversation is a container for a set of beans.
    30   *
    31   * <p>Optionally, a PersistenceContext can also be associated with a conversation.</p>
    32   *
    33   * <p>There are various ways how to get access to a Conversation instance:
    34   * <ul>
    35   * <li>{@link Conversation#getCurrentInstance} if you are calling from a
    36   * conversation-scoped bean, or something that is called from such a bean.</li>
    37   * <li>{@link ConversationManager#getConversation(String)}</li>
    38   * <li>by implementing the {@link ConversationAware} or {@link ConversationBindingListener}
    39   * interface in a bean.</li>
    40   * </ul>
    41   * </p>
    42   *
    43   * <p>Conversation instances are typically created when an EL expression references a
    44   * bean whose definition indicates that it is in a conversation scope.</p>
    45   *
    46   * <p>A conversation instance is typically destroyed:
    47   * <ul>
    48   * <li>At the end of a request when it are marked as access-scoped but
    49   * no bean in the conversation scope was accessed during the just-completed request.</li>
    50   * <li>Via an ox:endConversation component</li>
    51   * <li>Via an action method calling Conversation.invalidate()</li>
    52   * <li>Due to a conversation timeout, ie when no object in the conversation has been
    53   * accessed for N minutes. See ConversationManagedSessionListener,
    54   * ConversationTimeoutableAspect, and ConversationManager.checkTimeouts.</li>
    55   * </ul>
    56   * </p>
    57   */
    58  public class Conversation
    59  {
    60      // See getCurrentInstance, setCurrentInstance and class CurrentConversationAdvice.
    61      private final static ThreadLocal CURRENT_CONVERSATION = new ThreadLocal();
    62
    63      private final Log log = LogFactory.getLog(Conversation.class);
    64
    65      // The name of this conversation
    66      private final String name;
    67
    68      // The factory that created this conversation instance; needed
    69      // when restarting the conversation.
    70      private final ConversationFactory factory;
    71
    72      // The parent context to which this conversation belongs. This is needed
    73      // when restarting the conversation.
    74      private final ConversationContext conversationContext;
    75
    76      // An object that can bind arbitrary objects to this conversation so that all calls to
    77      // methods on the target object cause this conversation to become the active conversation,
    78      // and for any other optional "entry actions" (Advices) to be applied.
    79      private ConversationBinder binder;
    80
    81      // The set of managed beans that are associated with this conversation.
    82      private Map beans = new TreeMap();
    83
    84      // See addAspect.
    85      private ConversationAspects conversationAspects = new ConversationAspects();
    86
    87      // Is this object usable, or "destroyed"?
    88      private boolean invalid = false;
    89
    90      // Is this object going to be destroyed as soon as it is no longer "active"?
    91      private boolean queueInvalid = false;
    92
    93      // system timestamp in milliseconds at which this object was last accessed;
    94      // see method touch().
    95      private long lastAccess;
    96
    97      private Object activeCountMutex = new Object();
    98      private int activeCount;
    99
    100     public Conversation(ConversationContext conversationContext, String name, ConversationFactory factory)
    101     {
    102         this.conversationContext = conversationContext;
    103         this.name = name;
    104         this.factory = factory;
    105
    106         if (log.isDebugEnabled())
    107         {
    108             log.debug("start conversation:" + name);
    109         }
    110
    111         touch();
    112     }
    113
    114     /**
    115      * Define the (optional) binder used by this instance in method bind(Object).
    116      * <p>
    117      * Expected to be called by code that creates instances of this type immediately
    118      * after the constructor is invoked. See bind(Object) for more details.
    119      *
    120      * @since 1.3
    121      */
    122     public void setBinder(ConversationBinder binder)
    123     {
    124         this.binder = binder;
    125     }
    126
    127     /**
    128      * Mark this conversation as having been used at the current time.
    129      * <p>
    130      * Conversations can have "timeouts" associated with them, so that when a user stops
    131      * a conversation and goes off to work on some other part of the webapp then the
    132      * conversation's memory can eventually be reclaimed.
    133      * <p>
    134      * Whenever user code causes this conversation object to be looked up and returned,
    135      * this "touch" method is invoked to indicate that the conversation is in use. Direct
    136      * conversation lookups by user code can occur, but the most common access is expected
    137      * to be via an EL expression which a lookup of a bean that is declared as being in
    138      * conversation scope. The bean lookup causes the corresponding conversation to be
    139      * looked up, which triggers this method.
    140      */
    141     protected void touch()
    142     {
    143         lastAccess = System.currentTimeMillis();
    144     }
    145
    146     /**
    147      * The system time in millis when this conversation has been accessed last
    148      */
    149     public long getLastAccess()
    150     {
    151         return lastAccess;
    152     }
    153
    154     /**
    155      * Add the given bean to the conversation scope.
    156      *
    157      * <p>This will fire a {@link ConversationBindingEvent} on the bean parameter
    158      * object if the bean implements the {@link ConversationBindingListener}
    159      * interface</p>
    160      *
    161      * <p>Note that any object can be stored into the conversation; it is not
    162      * limited to managed beans declared in a configuration file. This
    163      * feature is not expected to be heavily used however; most attributes of
    164      * a conversation are expected to be externally-declared "managed beans".</p>
    165      */
    166     public void setAttribute(String name, Object bean)
    167     {
    168         checkValid();
    169
    170         synchronized(conversationContext)
    171         {
    172             removeAttribute(name);
    173
    174             if (log.isDebugEnabled())
    175             {
    176                 log.debug("put bean to conversation:" + name + "(bean=" + bean + ")");
    177             }
    178
    179             beans.put(name, bean);
    180         }
    181
    182         if (bean instanceof ConversationBindingListener)
    183         {
    184             ((ConversationBindingListener) bean).valueBound(
    185                 new ConversationBindingEvent(this, name));
    186         }
    187     }
    188
    189     /**
    190      * Assert the conversation is valid.
    191      *
    192      * Throws IllegalStateException if this conversation has been destroyed;
    193      * see method setInvalid.
    194      */
    195     protected void checkValid()
    196     {
    197         if (isInvalid())
    198         {
    199             throw new IllegalStateException("conversation '" + getName() + "' closed");
    200         }
    201     }
    202
    203     /**
    204      * Return the name of this conversation.
    205      * <p>
    206      * A conversation name is unique within a conversation context.
    207      */
    208     public String getName()
    209     {
    210         return name;
    211     }
    212
    213     /**
    214      * Return the factory that created this conversation.
    215      * <p>
    216      * Note that this factory will have set the initial aspects of this factory, which
    217      * configure such things as the lifetime (access, manual, etc) and conversation
    218      * timeout properties.
    219      */
    220     public ConversationFactory getFactory()
    221     {
    222         return factory;
    223     }
    224
    225     /**
    226      * Invalidate (end) the conversation.
    227      * <p>
    228      * If the conversation is currently active (ie the current call stack contains an object that
    229      * belongs to this conversation) then the conversation will just queue the object for later
    230      * destruction. Calls to methods like ConversationManager.getConversation(...) may still
    231      * return this object, and it will continue to function as a normal instance.
    232      * <p>
    233      * Only when the conversation is no longer active will the conversation (and the beans
    234      * it contains) actually be marked as invalid ("destroyed"). Once the conversation has been
    235      * destroyed, the ConversationManager will discard all references to it, meaning it will no
    236      * longer be accessable via lookups like ConversationManager.getConversation(). If something
    237      * does still have a reference to a destroyed conversation, then invoking almost any method
    238      * on that object will throw an IllegalStateException. In particular, adding a bean to the
    239      * conversation (invoking addAttribute) is not allowed.
    240      */
    241     public void invalidate()
    242     {
    243         if (!isActive())
    244         {
    245             destroy();
    246         }
    247         else
    248         {
    249             queueInvalid = true;
    250
    251             if (log.isDebugEnabled())
    252             {
    253                 log.debug("conversation '" + name + "' queued for destroy.");
    254             }
    255         }
    256     }
    257
    258     /**
    259      * Invalidate/End and restart the conversation.
    260      * <p>
    261      * This conversation object is immediately "destroyed" (see comments for method
    262      * invalidate), and a new instance is registered with the conversation manager
    263      * using the same name. The new instance is returned from this method.
    264      * <p>
    265      * Any code holding a reference to the old conversation instance will receive
    266      * an IllegalStateException when calling almost any method on that instance.
    267      *
    268      * @return the new conversation
    269      */
    270     public Conversation invalidateAndRestart()
    271     {
    272         String conversationName = getName();
    273         ConversationFactory factory = getFactory();
    274
    275         destroy();
    276
    277         return conversationContext.startConversation(conversationName, factory);
    278     }
    279
    280     /**
    281      * Return true if the conversation is invalid, ie should not be used.
    282      */
    283     public boolean isInvalid()
    284     {
    285         return invalid;
    286     }
    287
    288     /**
    289      * Return true if the conversation has been queued to be invalidated.
    290      */
    291     boolean isQueueInvalid()
    292     {
    293         return queueInvalid;
    294     }
    295
    296     /**
    297      * Destroy the conversation.
    298      * <ul>
    299      * <li>inform all beans implementing the {@link ConversationBindingListener} about the conversation end</li>
    300      * <li>free all beans</li>
    301      * </ul>
    302      * <p>
    303      * After return from this method, this conversation object's invalid flag is set and the map of
    304      * beans associated with this conversation is empty. In addition, the parent context no longer
    305      * holds a reference to this conversation.
    306      */
    307     protected void destroy()
    308     {
    309         if (log.isDebugEnabled())
    310         {
    311             log.debug("destroy conversation:" + name);
    312         }
    313
    314         synchronized(conversationContext)
    315         {
    316             String[] beanNames = (String[]) beans.keySet().toArray(new String[beans.size()]);
    317             for (int i = 0; i< beanNames.length; i++)
    318             {
    319                 removeAttribute(beanNames[i]);
    320             }
    321         }
    322
    323         conversationContext.removeConversation(getName());
    324
    325         invalid = true;
    326     }
    327
    328     /**
    329      * Check if this conversation holds a specific attribute (ie has a specific
    330      * named managed bean instance).
    331      */
    332     public boolean hasAttribute(String name)
    333     {
    334         synchronized(conversationContext)
    335         {
    336             return beans.containsKey(name);
    337         }
    338     }
    339
    340     /**
    341      * Get a specific attribute, ie a named managed bean.
    342      */
    343     public Object getAttribute(String name)
    344     {
    345         synchronized(conversationContext)
    346         {
    347             return beans.get(name);
    348         }
    349     }
    350
    351     /**
    352      * Remove a bean from the conversation.
    353      *
    354      * <p>This will fire a {@link ConversationBindingEvent} if the bean implements the
    355      * {@link ConversationBindingListener} interface.</p>
    356      */
    357     public Object removeAttribute(String name)
    358     {
    359         synchronized(conversationContext)
    360         {
    361             Object bean = beans.remove(name);
    362             if (bean instanceof ConversationBindingListener)
    363             {
    364                 ((ConversationBindingListener) bean).valueUnbound(
    365                     new ConversationBindingEvent(this, name));
    366             }
    367             return bean;
    368         }
    369     }
    370
    371     /**
    372      * Get the current conversation.
    373      *
    374      * @return The conversation object associated with the nearest object in the call-stack that
    375      * is configured to be in a conversation.<br />
    376      * If there is no object in the call-stack the system will lookup the single conversation
    377      * bound to the conversationContext.<br />
    378      * If not found, null will be returned.
    379      */
    380     public static Conversation getCurrentInstance()
    381     {
    382         CurrentConversationInfo conversation = getCurrentInstanceInfo();
    383         if (conversation != null)
    384         {
    385             return conversation.getConversation();
    386         }
    387
    388         return null;
    389     }
    390
    391     /**
    392      * Sets info about the current conversation instance.
    393      * <p>
    394      * This method is only expected to be called by CurrentConversationAdvice.invoke,
    395      * which ensures that the current instance is reset to null as soon as no bean
    396      * in the call-stack is within a conversation.
    397      */
    398     static void setCurrentInstance(CurrentConversationInfo conversation)
    399     {
    400         CURRENT_CONVERSATION.set(conversation);
    401     }
    402
    403     /**
    404      * Returns the info about the current conversation
    405      */
    406     static CurrentConversationInfo getCurrentInstanceInfo()
    407     {
    408         CurrentConversationInfo conversationInfo = (CurrentConversationInfo) CURRENT_CONVERSATION.get();
    409         if (conversationInfo != null && conversationInfo.getConversation() != null)
    410         {
    411             conversationInfo.getConversation().touch();
    412             return conversationInfo;
    413         }
    414
    415         return null;
    416     }
    417
    418     /**
    419      * Increase one to the "conversation active" counter.
    420      * <p>
    421      * This is called when a method is invoked on a bean that is within this conversation.
    422      * When the method returns, leaveConversation is invoked. The result is that the count
    423      * is greater than zero whenever there is a bean belonging to this conversation on
    424      * the callstack.
    425      * <p>
    426      * This method throws IllegalStateException if it is called on a conversation that has
    427      * been destroyed.
    428      */
    429     void enterConversation()
    430     {
    431         checkValid();
    432
    433         synchronized (activeCountMutex)
    434         {
    435             activeCount++;
    436         }
    437     }
    438
    439     /**
    440      * decrease one from the "conversation active" counter
    441      */
    442     void leaveConversation()
    443     {
    444         synchronized (activeCountMutex)
    445         {
    446             activeCount--;
    447         }
    448     }
    449
    450     /**
    451      * check if the conversation is active
    452      */
    453     private boolean isActive()
    454     {
    455         synchronized (activeCountMutex)
    456         {
    457             return activeCount > 0;
    458         }
    459     }
    460
    461     ConversationAspects getAspects()
    462     {
    463         return conversationAspects;
    464     }
    465
    466     /**
    467      * Get the aspect corresponding to the given class.
    468      *
    469      * @return null if such an aspect has not been attached to this conversation
    470      */
    471     public ConversationAspect getAspect(Class conversationAspectClass)
    472     {
    473         return conversationAspects.getAspect(conversationAspectClass);
    474     }
    475
    476     /**
    477      * Add an Aspect to this conversation.
    478      *
    479      * See class ConversationAspects for further details.
    480      */
    481     public void addAspect(ConversationAspect aspect)
    482     {
    483         conversationAspects.addAspect(aspect);
    484     }
    485
    486     /**
    487      * Get direct access to the beans map.
    488      * <p>
    489      * This method is only intended for use by subclasses that manipulate
    490      * the beans map in unusual ways. In general, it is better to use the
    491      * setAttribute/removeAttribute methods rather than accessing beans via
    492      * this map. Adding/removing entries in this map will not trigger the
    493      * usual callbacks on the bean objects themselves.
    494      *
    495      * @since 1.2
    496      */
    497     protected Map getBeans()
    498     {
    499         synchronized(conversationContext)
    500         {
    501             return beans;
    502         }
    503     }
    504
    505     /**
    506      * Replace the current beans map.
    507      * <p>
    508      * @see #getBeans()
    509      * @since 1.2
    510      */
    511     protected void setBeans(Map beans)
    512     {
    513         synchronized(conversationContext)
    514         {
    515             this.beans = beans;
    516         }
    517     }
    518
    519     /**
    520      * Return a proxy object that "binds" the specified instance to this conversation.
    521      * <p>
    522      * Whenever a method is executed on the proxy, this conversation is "entered" before the method
    523      * is invoked on the actual instance, as if the specified instance were a bean that was defined
    524      * in the dependency-injection framework as belonging to this conversation.
    525      * <p>
    526      * The specified bean is NOT added to the set of beans belonging to the conversation, ie its
    527      * lifecycle is still independent of the conversation.
    528      * <p>
    529      * The returned proxy is bound to a specific Conversation instance, so it should not be stored for
    530      * a long time; if the conversation is "invalidated" the proxy will continue to reference the original
    531      * conversation instance meaning that invoking the proxy would use the "stale" conversation rather than
    532      * a new instance. It also means that memory allocated to the conversation will not be recycled, although
    533      * this is not too serious as the invalidated conversation will be empty of beans. This limitation on
    534      * the lifetime of the returned proxy is not an issue for many of the uses of this method; in particular,
    535      * when wrapping objects returned by property getters for the use of EL expressions this is fine as the
    536      * proxy is only used during the scope of the EL expression execution. If a proxy is used after the
    537      * conversation it is bound to has been invalidated then an IllegalStateException will be thrown.
    538      * <p>
    539      * This method is useful when a conversation-scoped object creates an object instance via new() or via
    540      * calling some other library, and then wants all operations on that object to run within its own
    541      * conversation. In particular, when a backing-bean returns a persistent object that has been loaded
    542      * via a DAO class, it is often desirable for all methods on the persistent object to run within
    543      * the backing-bean's persistence context, ie within the same persistence context set up which
    544      * existed when the DAO class loaded the instance. For example, a JSF EL expression may retrieve
    545      * a persistent object from a backing bean then navigate through its properties; walking lazy
    546      * relations in this way will fail unless calls to methods of the persistent object cause the
    547      * correct persistence-context to be set up.
    548      * <p>
    549      * This method is theoretically an optional operation; the orchestra adapter layer for some specific
    550      * dependency-injection frameworks might choose not to support this, in which case an
    551      * UnsupportedOperationException will be thrown. The default Orchestra-Spring integration
    552      * certainly does support it.
    553      * <p>
    554      * It initially seems reasonable for Orchestra to also provide a variant of this method that
    555      * returns a "scoped proxy" object that looks up the most recent version of the conversation
    556      * by name and then runs the bound object in the context of that conversation instance. This
    557      * would mean that a proxy would never throw an IllegalStateException due to its conversation
    558      * having been invalidated. However this is not actually very useful. The primary use for this
    559      * method is expected to be wrapping of persistent objects returned by JPA, Hibernate or similar.
    560      * In this case the point is to access the object using the conversation's "persistence context";
    561      * a new conversation will have a new persistence context instance, not the one the object needs.
    562      *
    563      * @throw {@link UnsupportedOperationException}
    564      * @since 1.3
    565      */
    566     public Object bind(Object instance)
    567     {
    568         if (binder == null)
    569         {
    570             throw new UnsupportedOperationException("No beanBinder instance");
    571         }
    572         return binder.bind(instance);
    573     }
    574 }