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 // The set of managed beans that are associated with this conversation. 77 private Map beans = new TreeMap(); 78 79 // See addAspect. 80 private ConversationAspects conversationAspects = new ConversationAspects(); 81 82 // Is this object usable, or "destroyed"? 83 private boolean invalid = false; 84 85 // Is this object going to be destroyed as soon as it is no longer "active"? 86 private boolean queueInvalid = false; 87 88 // system timestamp in milliseconds at which this object was last accessed; 89 // see method touch(). 90 private long lastAccess; 91 92 private Object activeCountMutex = new Object(); 93 private int activeCount; 94 95 public Conversation(ConversationContext conversationContext, String name, ConversationFactory factory) 96 { 97 this.conversationContext = conversationContext; 98 this.name = name; 99 this.factory = factory; 100 101 if (log.isDebugEnabled()) 102 { 103 log.debug("start conversation:" + name); 104 } 105 106 touch(); 107 } 108 109 /** 110 * Mark this conversation as having been used at the current time. 111 * <p> 112 * Conversations can have "timeouts" associated with them, so that when a user stops 113 * a conversation and goes off to work on some other part of the webapp then the 114 * conversation's memory can eventually be reclaimed. 115 * <p> 116 * Whenever user code causes this conversation object to be looked up and returned, 117 * this "touch" method is invoked to indicate that the conversation is in use. Direct 118 * conversation lookups by user code can occur, but the most common access is expected 119 * to be via an EL expression which a lookup of a bean that is declared as being in 120 * conversation scope. The bean lookup causes the corresponding conversation to be 121 * looked up, which triggers this method. 122 */ 123 protected void touch() 124 { 125 lastAccess = System.currentTimeMillis(); 126 } 127 128 /** 129 * The system time in millis when this conversation has been accessed last 130 */ 131 public long getLastAccess() 132 { 133 return lastAccess; 134 } 135 136 /** 137 * Add the given bean to the conversation scope. 138 * 139 * <p>This will fire a {@link ConversationBindingEvent} on the bean parameter 140 * object if the bean implements the {@link ConversationBindingListener} 141 * interface</p> 142 * 143 * <p>Note that any object can be stored into the conversation; it is not 144 * limited to managed beans declared in a configuration file. This 145 * feature is not expected to be heavily used however; most attributes of 146 * a conversation are expected to be externally-declared "managed beans".</p> 147 */ 148 public void setAttribute(String name, Object bean) 149 { 150 checkValid(); 151 152 synchronized(conversationContext) 153 { 154 removeAttribute(name); 155 156 if (log.isDebugEnabled()) 157 { 158 log.debug("put bean to conversation:" + name + "(bean=" + bean + ")"); 159 } 160 161 beans.put(name, bean); 162 } 163 164 if (bean instanceof ConversationBindingListener) 165 { 166 ((ConversationBindingListener) bean).valueBound( 167 new ConversationBindingEvent(this, name)); 168 } 169 } 170 171 /** 172 * Assert the conversation is valid. 173 * 174 * Throws IllegalStateException if this conversation has been destroyed; 175 * see method setInvalid. 176 */ 177 protected void checkValid() 178 { 179 if (isInvalid()) 180 { 181 throw new IllegalStateException("conversation '" + getName() + "' closed"); 182 } 183 } 184 185 /** 186 * Return the name of this conversation. 187 * <p> 188 * A conversation name is unique within a conversation context. 189 */ 190 public String getName() 191 { 192 return name; 193 } 194 195 /** 196 * Return the factory that created this conversation. 197 * <p> 198 * Note that this factory will have set the initial aspects of this factory, which 199 * configure such things as the lifetime (access, manual, etc) and conversation 200 * timeout properties. 201 */ 202 public ConversationFactory getFactory() 203 { 204 return factory; 205 } 206 207 /** 208 * Invalidate (end) the conversation. 209 * <p> 210 * If the conversation is currently active (ie the current call stack contains an object that 211 * belongs to this conversation) then the conversation will just queue the object for later 212 * destruction. Calls to methods like ConversationManager.getConversation(...) may still 213 * return this object, and it will continue to function as a normal instance. 214 * <p> 215 * Only when the conversation is no longer active will the conversation (and the beans 216 * it contains) actually be marked as invalid ("destroyed"). Once the conversation has been 217 * destroyed, the ConversationManager will discard all references to it, meaning it will no 218 * longer be accessable via lookups like ConversationManager.getConversation(). If something 219 * does still have a reference to a destroyed conversation, then invoking almost any method 220 * on that object will throw an IllegalStateException. In particular, adding a bean to the 221 * conversation (invoking addAttribute) is not allowed. 222 */ 223 public void invalidate() 224 { 225 if (!isActive()) 226 { 227 destroy(); 228 } 229 else 230 { 231 queueInvalid = true; 232 233 if (log.isDebugEnabled()) 234 { 235 log.debug("conversation '" + name + "' queued for destroy."); 236 } 237 } 238 } 239 240 /** 241 * Invalidate/End and restart the conversation. 242 * <p> 243 * This conversation object is immediately "destroyed" (see comments for method 244 * invalidate), and a new instance is registered with the conversation manager 245 * using the same name. The new instance is returned from this method. 246 * <p> 247 * Any code holding a reference to the old conversation instance will receive 248 * an IllegalStateException when calling almost any method on that instance. 249 * 250 * @return the new conversation 251 */ 252 public Conversation invalidateAndRestart() 253 { 254 String conversationName = getName(); 255 ConversationFactory factory = getFactory(); 256 257 destroy(); 258 259 return conversationContext.startConversation(conversationName, factory); 260 } 261 262 /** 263 * Return true if the conversation is invalid, ie should not be used. 264 */ 265 public boolean isInvalid() 266 { 267 return invalid; 268 } 269 270 /** 271 * Return true if the conversation has been queued to be invalidated. 272 */ 273 boolean isQueueInvalid() 274 { 275 return queueInvalid; 276 } 277 278 /** 279 * Destroy the conversation. 280 * <ul> 281 * <li>inform all beans implementing the {@link ConversationBindingListener} about the conversation end</li> 282 * <li>free all beans</li> 283 * </ul> 284 */ 285 protected void destroy() 286 { 287 if (log.isDebugEnabled()) 288 { 289 log.debug("destroy conversation:" + name); 290 } 291 292 synchronized(conversationContext) 293 { 294 String[] beanNames = (String[]) beans.keySet().toArray(new String[beans.size()]); 295 for (int i = 0; i< beanNames.length; i++) 296 { 297 removeAttribute(beanNames[i]); 298 } 299 } 300 301 conversationContext.removeConversation(getName()); 302 303 invalid = true; 304 } 305 306 /** 307 * Check if this conversation holds a specific attribute (ie has a specific 308 * named managed bean instance). 309 */ 310 public boolean hasAttribute(String name) 311 { 312 synchronized(conversationContext) 313 { 314 return beans.containsKey(name); 315 } 316 } 317 318 /** 319 * Get a specific attribute, ie a named managed bean. 320 */ 321 public Object getAttribute(String name) 322 { 323 synchronized(conversationContext) 324 { 325 return beans.get(name); 326 } 327 } 328 329 /** 330 * Remove a bean from the conversation. 331 * 332 * <p>This will fire a {@link ConversationBindingEvent} if the bean implements the 333 * {@link ConversationBindingListener} interface.</p> 334 */ 335 public Object removeAttribute(String name) 336 { 337 synchronized(conversationContext) 338 { 339 Object bean = beans.remove(name); 340 if (bean instanceof ConversationBindingListener) 341 { 342 ((ConversationBindingListener) bean).valueUnbound( 343 new ConversationBindingEvent(this, name)); 344 } 345 return bean; 346 } 347 } 348 349 /** 350 * Get the current conversation. 351 * 352 * @return The conversation object associated with the nearest object in the call-stack that 353 * is configured to be in a conversation.<br /> 354 * If there is no object in the call-stack the system will lookup the single conversation 355 * bound to the conversationContext.<br /> 356 * If not found, null will be returned. 357 */ 358 public static Conversation getCurrentInstance() 359 { 360 CurrentConversationInfo conversation = getCurrentInstanceInfo(); 361 if (conversation != null) 362 { 363 return conversation.getConversation(); 364 } 365 366 return null; 367 } 368 369 /** 370 * Sets info about the current conversation instance. 371 * <p> 372 * This method is only expected to be called by CurrentConversationAdvice.invoke, 373 * which ensures that the current instance is reset to null as soon as no bean 374 * in the call-stack is within a conversation. 375 */ 376 static void setCurrentInstance(CurrentConversationInfo conversation) 377 { 378 CURRENT_CONVERSATION.set(conversation); 379 } 380 381 /** 382 * Returns the info about the current conversation 383 */ 384 static CurrentConversationInfo getCurrentInstanceInfo() 385 { 386 CurrentConversationInfo conversationInfo = (CurrentConversationInfo) CURRENT_CONVERSATION.get(); 387 if (conversationInfo != null && conversationInfo.getConversation() != null) 388 { 389 conversationInfo.getConversation().touch(); 390 return conversationInfo; 391 } 392 393 return null; 394 } 395 396 /** 397 * Increase one to the "conversation active" counter. 398 * 399 * This is called when a method is invoked on a bean that is within this conversation. 400 * When the method returns, leaveConversation is invoked. The result is that the count 401 * is greater than zero whenever there is a bean belonging to this conversation on 402 * the callstack. 403 */ 404 void enterConversation() 405 { 406 synchronized (activeCountMutex) 407 { 408 activeCount++; 409 } 410 } 411 412 /** 413 * decrease one from the "conversation active" counter 414 */ 415 void leaveConversation() 416 { 417 synchronized (activeCountMutex) 418 { 419 activeCount--; 420 } 421 } 422 423 /** 424 * check if the conversation is active 425 */ 426 private boolean isActive() 427 { 428 synchronized (activeCountMutex) 429 { 430 return activeCount > 0; 431 } 432 } 433 434 ConversationAspects getAspects() 435 { 436 return conversationAspects; 437 } 438 439 /** 440 * Get the aspect corresponding to the given class. 441 * 442 * @return null if such an aspect has not been attached to this conversation 443 */ 444 public ConversationAspect getAspect(Class conversationAspectClass) 445 { 446 return conversationAspects.getAspect(conversationAspectClass); 447 } 448 449 /** 450 * Add an Aspect to this conversation. 451 * 452 * See class ConversationAspects for further details. 453 */ 454 public void addAspect(ConversationAspect aspect) 455 { 456 conversationAspects.addAspect(aspect); 457 } 458 459 /** 460 * Get direct access to the beans map. 461 * <p> 462 * This method is only intended for use by subclasses that manipulate 463 * the beans map in unusual ways. In general, it is better to use the 464 * setAttribute/removeAttribute methods rather than accessing beans via 465 * this map. Adding/removing entries in this map will not trigger the 466 * usual callbacks on the bean objects themselves. 467 * 468 * @since 1.2 469 */ 470 protected Map getBeans() 471 { 472 synchronized(conversationContext) 473 { 474 return beans; 475 } 476 } 477 478 /** 479 * Replace the current beans map. 480 * <p> 481 * @see #getBeans() 482 * @since 1.2 483 */ 484 protected void setBeans(Map beans) 485 { 486 synchronized(conversationContext) 487 { 488 this.beans = beans; 489 } 490 } 491 }