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 }