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  import org.apache.myfaces.orchestra.lib.OrchestraException;
    25  import org.apache.myfaces.orchestra.lib._ReentrantLock;
    26
    27  import java.util.Arrays;
    28  import java.util.HashMap;
    29  import java.util.Iterator;
    30  import java.util.Map;
    31  import java.util.TreeMap;
    32  import java.util.Collection;
    33
    34  /**
    35   * A ConversationContext is a container for a set of conversations.
    36   * <p>
    37   * Normally there is only one ConversationContext per http session. However there can
    38   * be multiple instances if the user has multiple concurrent windows open into the same
    39   * webapp, using the ox:separateConversationContext or other similar mechanism.
    40   * <p>
    41   * Like the conversation class, a context can also have a timeout which will cause it
    42   * to be ended automatically if not accessed within the given period.
    43   */
    44  public class ConversationContext
    45  {
    46      private final Log log = LogFactory.getLog(ConversationContext.class);
    47
    48      // This id is attached as a query parameter to every url rendered in a page
    49      // (forms and links) so that if that url is invoked then the request will
    50      // cause the same context to be used from the user's http session.
    51      //
    52      // This value is never null, but an Object is used to store it rather than
    53      // a primitive long because it is used as a key into a collection of
    54      // conversation contexts, and using an object here saves wrapping this
    55      // value in a new object instance every time it must be used as a key.
    56      private final Long id;
    57
    58      // See addAttribute
    59      private final Map attributes = new TreeMap();
    60
    61      // the parent conversation context
    62      private final ConversationContext parent;
    63
    64      // The conversations held by this context, keyed by conversation name.
    65      private final Map conversations = new TreeMap();
    66
    67      /**
    68       * A name associated with this context
    69       */
    70      private String name;
    71
    72      // time at which this was last accessed, used for timeouts.
    73      private long lastAccess;
    74
    75      // default timeout for contexts: 30 minutes.
    76      private long timeoutMillis = 30 * 60 * 1000;
    77
    78      private final _ReentrantLock lock = new _ReentrantLock();
    79
    80      // Map of all child contexts of this context, keyed by child.id
    81      private Map childContexts = new HashMap();
    82
    83      /**
    84       * Constructor.
    85       */
    86      protected ConversationContext(long id)
    87      {
    88          this(null, id);
    89      }
    90
    91      /**
    92       * Constructor.
    93       *
    94       * @since 1.2
    95       */
    96      protected ConversationContext(ConversationContext parent, long id)
    97      {
    98          this.parent = parent;
    99          this.id = Long.valueOf(id);
    100
    101         if (parent != null)
    102         {
    103             parent.addChild(this);
    104         }
    105
    106         touch();
    107     }
    108
    109     /**
    110      * Get the name associated to this context.
    111      *
    112      * @since 1.2
    113      */
    114     public String getName()
    115     {
    116         return name;
    117     }
    118
    119     /**
    120      * Assign a name to this context.
    121      *
    122      * @since 1.2
    123      */
    124     public void setName(String name)
    125     {
    126         this.name = name;
    127     }
    128
    129     /**
    130      * The conversation context id, unique within the current http session.
    131      */
    132     public long getId()
    133     {
    134         return id.longValue();
    135     }
    136
    137     /**
    138      * The conversation context id, unique within the current http session.
    139      */
    140     public Long getIdAsLong()
    141     {
    142         return id;
    143     }
    144
    145     /**
    146      * Return the parent conversation context (if any).
    147      *
    148      * @since 1.2
    149      */
    150     public ConversationContext getParent()
    151     {
    152         return parent;
    153     }
    154
    155     /**
    156      * @since 1.3
    157      */
    158     public void addChild(ConversationContext context)
    159     {
    160         childContexts.put(context.getIdAsLong(), context);
    161     }
    162
    163     /**
    164      * @since 1.4
    165      */
    166     protected Collection getChildren()
    167     {
    168         return childContexts.values();
    169     }
    170
    171     /**
    172      * @since 1.3
    173      */
    174     public void removeChild(ConversationContext context)
    175     {
    176         Object o = childContexts.remove(context.getIdAsLong());
    177         if (o != context)
    178         {
    179             // Sanity check failed: o is null, or o is a different object.
    180             // In either case, something is very wrong.
    181             throw new OrchestraException("Invalid call of removeChild");
    182         }
    183     }
    184
    185     /**
    186      * @since 1.3
    187      */
    188     public boolean hasChildren()
    189     {
    190         return !childContexts.isEmpty();
    191     }
    192
    193     /**
    194      * Mark this context as having been used.
    195      */
    196     protected void touch()
    197     {
    198         lastAccess = System.currentTimeMillis();
    199
    200         if (getParent() != null)
    201         {
    202             getParent().touch();
    203         }
    204     }
    205
    206     /**
    207      * The system time in millis when this conversation has been accessed last.
    208      */
    209     public long getLastAccess()
    210     {
    211         return lastAccess;
    212     }
    213
    214     /**
    215      * Get the timeout after which this context will be closed.
    216      *
    217      * @see #setTimeout
    218      */
    219     public long getTimeout()
    220     {
    221         return timeoutMillis;
    222     }
    223
    224     /**
    225      * Set the timeout after which this context will be closed.
    226      * <p>
    227      * A value of -1 means no timeout checking.
    228      */
    229     public void setTimeout(long timeoutMillis)
    230     {
    231         this.timeoutMillis = timeoutMillis;
    232     }
    233
    234     /**
    235      * Invalidate all conversations within this context.
    236      *
    237      * @deprecated Use the "invalidate" method instead.
    238      */
    239     protected void clear()
    240     {
    241         invalidate();
    242     }
    243
    244     /**
    245      * Invalidate all conversations within this context.
    246      *
    247      * @since 1.3
    248      */
    249     protected void invalidate()
    250     {
    251         synchronized (this)
    252         {
    253             Conversation[] convArray = new Conversation[conversations.size()];
    254             conversations.values().toArray(convArray);
    255
    256             for (int i = 0; i < convArray.length; i++)
    257             {
    258                 Conversation conversation = convArray[i];
    259                 conversation.invalidate();
    260             }
    261
    262             conversations.clear();
    263         }
    264     }
    265
    266     /**
    267      * Start a conversation if not already started.
    268      */
    269     protected Conversation startConversation(String name, ConversationFactory factory)
    270     {
    271         synchronized (this)
    272         {
    273             touch();
    274             Conversation conversation = (Conversation) conversations.get(name);
    275             if (conversation == null)
    276             {
    277                 conversation = factory.createConversation(this, name);
    278
    279                 conversations.put(name, conversation);
    280             }
    281             return conversation;
    282         }
    283     }
    284
    285     /**
    286      * Remove the conversation from this context.
    287      *
    288      * <p>Notice: It is assumed that the conversation has already been invalidated.</p>
    289      */
    290     protected void removeConversation(Conversation conversation)
    291     {
    292         synchronized (this)
    293         {
    294             touch();
    295             conversations.remove(conversation.getName());
    296         }
    297     }
    298
    299     /**
    300      * Remove the conversation with the given name from this context.
    301      *
    302      * <p>Notice: Its assumed that the conversation has already been invalidated</p>
    303      */
    304     protected void removeConversation(String name)
    305     {
    306         synchronized (this)
    307         {
    308             touch();
    309             Conversation conversation = (Conversation) conversations.get(name);
    310             if (conversation != null)
    311             {
    312                 removeConversation(conversation);
    313             }
    314         }
    315     }
    316
    317     /**
    318      * Return true if there are one or more conversations in this context.
    319      */
    320     protected boolean hasConversations()
    321     {
    322         synchronized (this)
    323         {
    324             touch();
    325             return conversations.size() > 0;
    326         }
    327     }
    328
    329     /**
    330      * Check if the given conversation exists.
    331      */
    332     protected boolean hasConversation(String name)
    333     {
    334         synchronized (this)
    335         {
    336             touch();
    337             return conversations.get(name) != null;
    338         }
    339     }
    340
    341     /**
    342      * Get a conversation by name.
    343      * <p>
    344      * This looks only in the current context, not in any child contexts.
    345      */
    346     protected Conversation getConversation(String name)
    347     {
    348         synchronized (this)
    349         {
    350             touch();
    351
    352             Conversation conv = (Conversation) conversations.get(name);
    353             if (conv != null)
    354             {
    355                 conv.touch();
    356             }
    357
    358             return conv;
    359         }
    360     }
    361
    362     /**
    363      * Iterates over all the conversations in this context.
    364      * <p>
    365      * This does not include conversations in parent contexts.
    366      *
    367      * @return An iterator over a copy of the conversation list. It is safe to remove objects from
    368      * the conversation list while iterating, as the iterator refers to a different collection.
    369      */
    370     public Iterator iterateConversations()
    371     {
    372         synchronized (this)
    373         {
    374             touch();
    375
    376             Conversation[] convs = (Conversation[]) conversations.values().toArray(
    377                     new Conversation[conversations.size()]);
    378             return Arrays.asList(convs).iterator();
    379         }
    380     }
    381
    382     /**
    383      * Check the timeout for every conversation in this context.
    384      * <p>
    385      * This method does not check the timeout for this context object itself.
    386      */
    387     protected void checkConversationTimeout()
    388     {
    389         synchronized (this)
    390         {
    391             Conversation[] convArray = new Conversation[conversations.size()];
    392             conversations.values().toArray(convArray);
    393
    394             for (int i = 0; i < convArray.length; i++)
    395             {
    396                 Conversation conversation = convArray[i];
    397
    398                 ConversationTimeoutableAspect timeoutAspect =
    399                     (ConversationTimeoutableAspect)
    400                         conversation.getAspect(ConversationTimeoutableAspect.class);
    401
    402                 if (timeoutAspect != null && timeoutAspect.isTimeoutReached())
    403                 {
    404                     if (log.isDebugEnabled())
    405                     {
    406                         log.debug("end conversation due to timeout: " + conversation.getName());
    407                     }
    408
    409                     conversation.invalidate();
    410                 }
    411             }
    412         }
    413     }
    414
    415     /**
    416      * Add an attribute to the conversationContext.
    417      * <p>
    418      * A context provides a map into which any arbitrary objects can be stored. It
    419      * isn't a major feature of the context, but can occasionally be useful.
    420      */
    421     public void setAttribute(String name, Object attribute)
    422     {
    423         synchronized(attributes)
    424         {
    425             attributes.remove(name);
    426             attributes.put(name, attribute);
    427         }
    428     }
    429
    430     /**
    431      * Check if this conversationContext holds a specific attribute.
    432      */
    433     public boolean hasAttribute(String name)
    434     {
    435         synchronized(attributes)
    436         {
    437             return attributes.containsKey(name);
    438         }
    439     }
    440
    441     /**
    442      * Get a specific attribute.
    443      */
    444     public Object getAttribute(String name)
    445     {
    446         synchronized(attributes)
    447         {
    448             return attributes.get(name);
    449         }
    450     }
    451
    452     /**
    453      * Remove an attribute from the conversationContext.
    454      */
    455     public Object removeAttribute(String name)
    456     {
    457         synchronized(attributes)
    458         {
    459             return attributes.remove(name);
    460         }
    461     }
    462
    463     /**
    464      * Block until no other thread has this instance marked as reserved, then
    465      * mark the object as reserved for this thread.
    466      * <p>
    467      * It is safe to call this method multiple times.
    468      * <p>
    469      * If this method is called, then an equal number of calls to
    470      * unlockForCurrentThread <b>MUST</b> made, or this context object
    471      * will remain locked until the http session times out.
    472      * <p>
    473      * Note that this method may be called very early in the request processing
    474      * lifecycle, eg before a FacesContext exists for a JSF request.
    475      *
    476      * @since 1.1
    477      */
    478     public void lockInterruptablyForCurrentThread() throws InterruptedException
    479     {
    480         if (log.isDebugEnabled())
    481         {
    482             log.debug("Locking context " + this.id);
    483         }
    484         lock.lockInterruptibly();
    485     }
    486
    487     /**
    488      * Block until no other thread has this instance marked as reserved, then
    489      * mark the object as reserved for this thread.
    490      * <p>
    491      * Note that this method may be called very late in the request processing
    492      * lifecycle, eg after a FacesContext has been destroyed for a JSF request.
    493      *
    494      * @since 1.1
    495      */
    496     public void unlockForCurrentThread()
    497     {
    498         if (log.isDebugEnabled())
    499         {
    500             log.debug("Unlocking context " + this.id);
    501         }
    502         lock.unlock();
    503     }
    504
    505     /**
    506      * Return true if this object is currently locked by the calling thread.
    507      *
    508      * @since 1.1
    509      */
    510     public boolean isLockedForCurrentThread()
    511     {
    512         return lock.isHeldByCurrentThread();
    513     }
    514
    515     /**
    516      * Get the root conversation context this conversation context is
    517      * associated with.
    518      * <p>
    519      * This is equivalent to calling getParent repeatedly until a context
    520      * with no parent is found.
    521      *
    522      * @since 1.2
    523      */
    524     public ConversationContext getRoot()
    525     {
    526         ConversationContext cctx = this;
    527         while (cctx != null && cctx.getParent() != null)
    528         {
    529             cctx = getParent();
    530         }
    531
    532         return cctx;
    533     }
    534 }