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 }