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