From 83856555c90713e4757a2e40065b0d5be97d02ff Mon Sep 17 00:00:00 2001 From: Hailton de Castro Date: Fri, 8 Jun 2012 22:52:18 -0300 Subject: [PATCH] Conversation Workaround (Core Implementation) --- .../AssemblyInfo.cs | 5 + .../HttpModule/ConversationModule.cs | 125 ++++ .../ConversationWA/IConversationManager.cs | 92 +++ .../ConversationWA/IConversationState.cs | 172 +++++ .../Imple/InnerConversationList.cs | 354 +++++++++++ .../Imple/WebConversationManager.cs | 295 +++++++++ .../Imple/WebConversationSpringState.cs | 601 ++++++++++++++++++ .../Support/SessionPerConversationScope.cs | 450 +++++++++++++ .../Support/SessionPerConversationSettings.cs | 97 +++ .../Spring.ConversationWA.NH32.2008.csproj | 103 +++ .../Spring.ConversationWA.NH32.2010.csproj | 102 +++ .../Spring.ConversationWA.NH32.build | 45 ++ .../Spring.ConversationWA.NH32/app.config | 3 + .../AssemblyInfo.cs | 5 + .../ConversationWA/HttpModule/dontdelete.txt | 1 + .../ConversationWA/Imple/dontdelete.txt | 1 + .../ConversationWA/dontdelete.txt | 1 + .../Data/NHibernate/Support/dontdelete.txt | 1 + .../Spring.ConversationWA.NH33.2008.csproj | 112 ++++ .../Spring.ConversationWA.NH33.2010.csproj | 114 ++++ .../Spring.ConversationWA.NH33.build | 47 ++ .../Spring.ConversationWA.NH33/app.config | 3 + 22 files changed, 2729 insertions(+) create mode 100644 src/Spring/Spring.ConversationWA.NH32/AssemblyInfo.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/ConversationWA/HttpModule/ConversationModule.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationManager.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationState.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/InnerConversationList.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationManager.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationSpringState.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationScope.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationSettings.cs create mode 100644 src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2008.csproj create mode 100644 src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2010.csproj create mode 100644 src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.build create mode 100644 src/Spring/Spring.ConversationWA.NH32/app.config create mode 100644 src/Spring/Spring.ConversationWA.NH33/AssemblyInfo.cs create mode 100644 src/Spring/Spring.ConversationWA.NH33/ConversationWA/HttpModule/dontdelete.txt create mode 100644 src/Spring/Spring.ConversationWA.NH33/ConversationWA/Imple/dontdelete.txt create mode 100644 src/Spring/Spring.ConversationWA.NH33/ConversationWA/dontdelete.txt create mode 100644 src/Spring/Spring.ConversationWA.NH33/Data/NHibernate/Support/dontdelete.txt create mode 100644 src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2008.csproj create mode 100644 src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2010.csproj create mode 100644 src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.build create mode 100644 src/Spring/Spring.ConversationWA.NH33/app.config diff --git a/src/Spring/Spring.ConversationWA.NH32/AssemblyInfo.cs b/src/Spring/Spring.ConversationWA.NH32/AssemblyInfo.cs new file mode 100644 index 00000000..d82e84c0 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: AssemblyTitle("Spring.ConversationWA.NH32. NHibernate 3.2 support.")] +[assembly: AssemblyDescription("Interfaces and classes that provide 'Conversation Workaround' support in Spring.Net")] \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH32/ConversationWA/HttpModule/ConversationModule.cs b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/HttpModule/ConversationModule.cs new file mode 100644 index 00000000..4804eeb6 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/HttpModule/ConversationModule.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Web; +using Spring.Context.Support; +using Common.Logging; +using Spring.Data.NHibernate.Support; +using System.Web.UI; +using Spring.Context; + +namespace Spring.ConversationWA.HttpModule +{ + /// + /// HttpModule for end Conversation with Timeout exceeded. + /// + /// Hailton de Castro + public class ConversationModule : IHttpModule, IApplicationContextAware + { + private static readonly ILog LOG = LogManager.GetLogger(typeof(ConversationModule)); + + /// + /// Default constructor. + /// + public ConversationModule(){ } + + private IList conversationManagerName; + /// + /// Name for the IConversationManager on the spring context. + /// + public IList ConversationManagerNameList + { + get { return conversationManagerName; } + set { conversationManagerName = value; } + } + + #region IHttpModule Members + + /// + /// Add PostRequestHandlerExecute event to clear conversations with timeout exceeded. + /// + /// + public void Init(HttpApplication context) + { + context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute); + context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute); + context.EndRequest += new EventHandler(context_EndRequest); + } + + /// + /// NOOP. + /// + public void Dispose() + { + //noop + } + + void context_PreRequestHandlerExecute(object sender, EventArgs e) + { + if (HttpContext.Current.Handler is Page) + { + Page page = (Page)HttpContext.Current.Handler; + page.Unload += new EventHandler(page_Unload); + + if (HttpContext.Current.Session != null) + { + if (LOG.IsDebugEnabled) LOG.Debug("context_PreRequestHandlerExecute: HttpContext.Current.Session is NOT null"); + foreach (String convMngName in this.ConversationManagerNameList) + { + IConversationManager convMng = (IConversationManager)this.applicationContext.GetObject(convMngName); + convMng.EndOnTimeOut(); + convMng.FreeEnded(); + } + } + else + { + if (LOG.IsDebugEnabled) LOG.Debug("context_PreRequestHandlerExecute: HttpContext.Current.Session IS null"); + } + } + } + + /// + /// Necessary for Redirect or Abort for some reason. + /// + /// + /// + void page_Unload(object sender, EventArgs e) + { + if (LOG.IsDebugEnabled) LOG.Debug("page_Unload HttpContext.Current.Session is null: " + (HttpContext.Current.Session == null)); + foreach (String convMngName in this.ConversationManagerNameList) + { + IConversationManager convMng = (IConversationManager)this.applicationContext.GetObject(convMngName); + convMng.EndOnTimeOut(); + convMng.FreeEnded(); + convMng.PauseConversations(); + } + } + + void context_EndRequest(object sender, EventArgs e) + { + if (LOG.IsDebugEnabled) LOG.Debug("context_EndRequest HttpContext.Current.Session is null: " + (HttpContext.Current.Session == null)); + } + + void context_PostRequestHandlerExecute(object sender, EventArgs e) + { + if (LOG.IsDebugEnabled) LOG.Debug("context_PostRequestHandlerExecute HttpContext.Current.Session is null: " + (HttpContext.Current.Session == null)); + if (HttpContext.Current.Session != null) + { + } + } + + #endregion + + #region IApplicationContextAware Members + private IApplicationContext applicationContext; + /// + /// Used to obtain the instances of "IConversationManager". + /// + public IApplicationContext ApplicationContext + { + set { this.applicationContext = value; } + } + + #endregion + } +} diff --git a/src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationManager.cs b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationManager.cs new file mode 100644 index 00000000..5ec3c961 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationManager.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NHibernate; + +namespace Spring.ConversationWA +{ + /// + /// manager for Conversations. + /// + /// Hailton de Castro + public interface IConversationManager + { + /// + /// Returns the conversation if it is still alive, otherwise it returns null. + /// + /// + /// + IConversationState GetConversationById(String id); + + /// + /// Ends all conversations with the timeout exceeded. + /// + void EndOnTimeOut(); + + /// + /// Close IDbConnection's for that + /// use 'session-per-conversation'. It calls + /// in all conversations. + /// + void PauseConversations(); + + /// + /// Release the ended conversatons And remove it. + /// If the conversation support 'session-per-conversation' close the session. + /// + void FreeEnded(); + + /// + /// Add conversation. If is null + /// it is setted with 'this'. + /// + /// + /// + /// If already has another manager. + /// + void AddConversation(IConversationState conversation); + + /// + /// Makes the 'root conversation' of + /// the current active conversation and open/reopen the + /// if + /// the conversation supports 'session-per-conversation'. Close all + /// the connection for all session before. + /// If is true will end all + /// paused conversations. + /// + void SetActiveConversation(IConversationState conversation); + + /// + /// Returns the active conversation if exists, otherwise returns null. + /// It depends on + /// + /// + IConversationState ActiveConversation{ get; } + + /// + /// If this is non-null run pattern 'session-per-conversation'. + /// Must be the same SessionFactory of the managed conversations. + /// + /// + ISessionFactory SessionFactory { get; set; } + + /// + /// Ends the "paused conversations" in call to . + /// Important: Unexpected behavior may occur if there are nested conversations, + /// as in 'StartResumeConversation' only the own conversation and their parents + /// are started, the 'conversations children' remain paused, so these will be ended. + /// Defaul value: false. + /// + /// + /// When it is true, "start/resume a conversation" will cause the other to be + /// ended and cleaned up. + /// + /// This is useful to avoid memory leak where there are many conversations. + /// This leak can be very considerable, as the conversation may keep a "NHibernate session" + /// that can contain many objects in its cache from the database queries. + /// + /// + bool EndPaused { get; set; } + } +} diff --git a/src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationState.cs b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationState.cs new file mode 100644 index 00000000..2338a910 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/IConversationState.cs @@ -0,0 +1,172 @@ +using System; +using System.Data; +using System.Configuration; +using System.Web; +using System.Web.Security; +using System.Web.UI; +using System.Web.UI.WebControls; +using System.Web.UI.WebControls.WebParts; +using System.Web.UI.HtmlControls; +using System.Collections.Generic; +using System.Collections; +using NHibernate; +using Spring.Data.Common; + +namespace Spring.ConversationWA +{ + /// + /// Port to conversation. If the object is not found in the current + /// conversation, will be tried on the parent if the parent is + /// different not null. + /// + /// + /// If is different from spring name for this instance. + /// + /// Hailton de Castro + public interface IConversationState: IDictionary + { + /// + /// Conversation id. + /// + String Id { get; set; } + + /// + /// Starts or resumes the conversation and the . + /// If is not null, so + /// .GetCurrentSession() is called to + /// Raise SessionHolder for make the reconnection. + /// + /// Make return false. + /// + /// Update the . + /// + /// + /// + /// + /// If this conversation is ended. + /// + /// If is not null and + /// different from + /// .GetCurrentSession() + /// + /// + /// + void StartResumeConversation(); + + /// + /// Return true until is called. + /// + bool IsNew { get; } + + /// + /// Ends the conversation. End each the 'inner conversation' in + /// . Returns false if the + /// conversation and all of + /// has already been ended. + /// + /// + /// + /// + /// If . + /// Session["spring.objects"] + /// is null. + /// + /// The 'spring session scopes' are not located in the key + /// 'spring.objects' of HttpSessionState. + /// + /// + /// + void EndConversation(); + + /// + /// Return true if this conversation is ended. + /// + bool Ended { get; } + + /// + /// Inner conversation. After added if the + /// is null it will be setted with 'this'. + /// + /// at + /// , + /// , + /// + /// if Circular Dependence is detected. + IList InnerConversations { get; } + + /// + /// Conversation parent. + /// + /// + /// + /// If this conversation already has a different parent. + /// + /// If this Conversation is not new. + /// + /// If Circular Dependence is detected. + /// + /// The Parent conversation is not new. + /// + /// + /// + IConversationState ParenteConversation { get; set;} + + /// + /// TimeOut for the conversation in milliseconds. + /// If 0 means it will be ignored. + /// + Int32 TimeOut { get; set; } + + /// + /// Last acces for a value into this Conversation or Inner Conversation. + /// It is reseted for DateTime.Now each time + /// is called. + /// + DateTime LastAccess { get; set; } + + /// + /// Conversation Manager. When this is setted if + /// + /// returns null so AddConversation is called. + /// + IConversationManager ConversationManager { get; set; } + + /// + /// that is stored in the root conversation. + /// + /// + /// must support 'session-per-conversation'. + /// + /// + ISession RootSessionPerConversation { get; set; } + + /// + /// If this is non-null run pattern c. + /// It also depends on and . + /// must support ConversationManager. + /// + /// + ISessionFactory SessionFactory { get; set; } + + /// + /// If this is non-null run pattern 'session-per-conversation'. + /// It also depends on and . + /// must support ConversationManager. + /// + /// + IDbProvider DbProvider { get; set; } + + /// + /// Indicates that the conversation is paused. + /// + bool IsPaused { get; } + + /// + /// Starts or resumes the conversation and each 'inner conversation' in + /// . + /// It is not about 'Session-per-conversation' because it is done by + /// . + /// + void PauseConversation(); + } +} diff --git a/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/InnerConversationList.cs b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/InnerConversationList.cs new file mode 100644 index 00000000..83ae531b --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/InnerConversationList.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; +using Common.Logging; +using Iesi.Collections.Generic; + +namespace Spring.ConversationWA.Imple +{ + /// + /// List that make validation for Circular Dependence for + /// + /// Hailton de Castro + public class InnerConversationList: IList, IList + { + private static readonly ILog LOG = LogManager.GetLogger(typeof(InnerConversationList)); + + private IConversationState conversationOwner; + /// + /// Contructor. + /// + /// The IConversation that owns this InnerConversationList. + public InnerConversationList(IConversationState conversationOwner) + { + if (conversationOwner == null) + { + String exMsgStr = "'conversationOwner' can not be null"; + + LOG.Error(exMsgStr); + throw new InvalidOperationException(exMsgStr); + } + if (LOG.IsDebugEnabled) LOG.Error(String.Format("Creating InnerConversationList for '{0}'", conversationOwner.Id)); + this.conversationOwner = conversationOwner; + } + + private IList innerList = new List(); + + /// + /// Common Helper to be run before insert. + /// + /// + private void addPreAddHelper(IConversationState itemAdded) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("addPreAddHelper: added={0} into {1}", itemAdded, this.conversationOwner)); + this.validateCircularDependence(itemAdded); + if (itemAdded.ParenteConversation != null && itemAdded.ParenteConversation != this.conversationOwner) + { + throw new InvalidOperationException( + String.Format(WebConversationSpringState.MSG_CONVERSATION_ALREADY_HAS_PARENT, itemAdded.ParenteConversation.Id, this.conversationOwner.Id)); + } + } + + /// + /// Common Helper to be run after insert. + /// + private void addPostAddHelper(IConversationState itemAdded) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("addPreAddHelper: added={0} into {1}", itemAdded, this.conversationOwner)); + if (itemAdded.ParenteConversation == null) + { + itemAdded.ParenteConversation = this.conversationOwner; + } + } + + private void validateCircularDependence(IConversationState itemAdded) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("Validating Circular Dependence: added={0} into {1}", itemAdded, this.conversationOwner)); + + ICollection visitedColl = new HashedSet(); + visitedColl.Add(conversationOwner); + + IConversationState parentAncestor = this.conversationOwner; + String path = this.conversationOwner.Id; + //string conectorPath = ""; + + this.validateCircularDependenceRecursive(itemAdded, visitedColl, path + "->" + itemAdded.Id); + } + + private void validateCircularDependenceRecursive(IConversationState currentConv, ICollection visitedColl, String path) + { + foreach (IConversationState convItem in currentConv.InnerConversations) + { + if (visitedColl.Contains(convItem)) + { + String exMsgStr = + "ConversationState Circular Dependence detected: " + + path + "->" + convItem.Id; + + LOG.Error(exMsgStr); + throw new InvalidOperationException(exMsgStr); + } + + visitedColl.Add(convItem); + + this.validateCircularDependenceRecursive(convItem, visitedColl, path + "->" + convItem.Id); + } + } + + #region IList Members + /// + /// + /// + /// + /// + public int IndexOf(IConversationState item) + { + return this.innerList.IndexOf(item); + } + + /// + /// + /// + /// + /// + public void Insert(int index, IConversationState item) + { + this.addPreAddHelper(item); + this.innerList.Insert(index, item); + this.addPostAddHelper(item); + } + + /// + /// + /// + /// + public void RemoveAt(int index) + { + this.innerList.RemoveAt(index); + } + + /// + /// + /// + /// + /// + public IConversationState this[int index] + { + get + { + return this.innerList[index]; + } + set + { + this.addPreAddHelper(value); + this.innerList[index] = value; + this.addPostAddHelper(value); + } + } + + #endregion + + #region ICollection Members + /// + /// + /// + /// + public void Add(IConversationState item) + { + this.addPreAddHelper(item); + this.innerList.Add(item); + this.addPostAddHelper(item); + } + + /// + /// + /// + public void Clear() + { + this.innerList.Clear(); + } + + /// + /// + /// + /// + /// + public bool Contains(IConversationState item) + { + return this.innerList.Contains(item); + } + + /// + /// + /// + /// + /// + public void CopyTo(IConversationState[] array, int arrayIndex) + { + this.innerList.CopyTo(array, arrayIndex); + } + + /// + /// + /// + public int Count + { + get { return this.innerList.Count; } + } + + /// + /// + /// + public bool IsReadOnly + { + get { return this.innerList.IsReadOnly; } + } + + /// + /// + /// + /// + /// + public bool Remove(IConversationState item) + { + return this.innerList.Remove(item); + } + + #endregion + + #region IEnumerable Members + /// + /// + /// + /// + public IEnumerator GetEnumerator() + { + return this.innerList.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + /// + /// + /// + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.innerList.GetEnumerator(); + } + + #endregion + + #region IList Members + /// + /// + /// + /// + /// + public int Add(object value) + { + ((IList)this).Add((IConversationState)value); + return this.innerList.Count - 1; + } + + /// + /// + /// + /// + /// + public bool Contains(object value) + { + return ((IList)this).Contains((IConversationState)value); + } + + /// + /// + /// + /// + /// + public int IndexOf(object value) + { + return ((IList)this).IndexOf((IConversationState)value); + } + + /// + /// + /// + /// + /// + public void Insert(int index, object value) + { + ((IList)this).Insert(index, (IConversationState)value); + } + + /// + /// + /// + public bool IsFixedSize + { + get { return false; } + } + + /// + /// + /// + /// + public void Remove(object value) + { + ((IList)this).Add((IConversationState)value); + } + + /// + /// + /// + /// + /// + object IList.this[int index] + { + get + { + return ((IList)this)[index]; + } + set + { + ((IList)this)[index] = (IConversationState)value; + } + } + + #endregion + + #region ICollection Members + /// + /// + /// + /// + /// + public void CopyTo(Array array, int index) + { + IConversationState[] convArr = new IConversationState[array.Length]; + ((IList)this).CopyTo(convArr, index); + convArr.CopyTo(array, index); + } + + /// + /// + /// + public bool IsSynchronized + { + get { return false; } + } + + /// + /// + /// + public object SyncRoot + { + get { return this; } + } + + #endregion + } +} diff --git a/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationManager.cs b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationManager.cs new file mode 100644 index 00000000..8fe66e79 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationManager.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Web; +using Common.Logging; +using Spring.Data.NHibernate.Support; +using NHibernate; + +namespace Spring.ConversationWA.Imple +{ + /// + /// This was made to stay under session scope. + /// + /// Hailton de Castro + public class WebConversationManager : SessionPerConversationScope, IConversationManager + { + private static readonly ILog LOG = LogManager.GetLogger(typeof(WebConversationManager)); + + private static readonly String CONVERSATION_COOKIE_ID = "WebConversationManager.activeConversationId"; + + /// + /// Semaphore to synchronize writes to the dictionary. + /// + private Mutex mutexEditDic = new Mutex(); + private IDictionary conversations = new Dictionary(); + private IConversationState activeConversation = null; + + #region IConversationManager Members + + /// + /// + /// + /// + /// + public IConversationState GetConversationById(string id) + { + if (this.conversations.ContainsKey(id)) + { + return this.conversations[id]; + } + else + { + return null; + } + } + + /// + /// + /// + public void EndOnTimeOut() + { + try + { + if (LOG.IsDebugEnabled) LOG.Debug("EndOnTimeOut"); + this.mutexEditDic.WaitOne(5000); + foreach (String keyItem in this.conversations.Keys) + { + IConversationState conversationItem = this.conversations[keyItem]; + if (conversationItem.TimeOut > 0) + { + if (DateTime.Now.Subtract(conversationItem.LastAccess).TotalMilliseconds > conversationItem.TimeOut) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("Timeout for conversation '{0}'", conversationItem.Id)); + conversationItem.EndConversation(); + } + } + } + } + finally + { + this.mutexEditDic.ReleaseMutex(); + } + } + + /// + /// + /// + public void PauseConversations() + { + foreach (IConversationState convItem in this.conversations.Values) + { + convItem.PauseConversation(); + } + this.Close(this.SessionFactory, this.conversations.Values); + } + + /// + /// + /// + public void FreeEnded() + { + try + { + if (LOG.IsDebugEnabled) LOG.Debug("EndOnTimeOut"); + this.mutexEditDic.WaitOne(5000); + List removeList = new List(); + foreach (String keyItem in this.conversations.Keys) + { + IConversationState conversationItem = this.conversations[keyItem]; + if (conversationItem.Ended) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("FreeEnded: Release conversation '{0}'", conversationItem.Id)); + removeList.Add(conversationItem); + } + } + + if (removeList.Count > 0) + { + this.Close(this.sessionFactory, removeList); + } + + foreach (IConversationState conversationItem in removeList) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("FreeEnded: Remove conversation '{0}'", conversationItem.Id)); + conversationItem.EndConversation(); + this.RemoveConversation(conversationItem); + } + } + finally + { + this.mutexEditDic.ReleaseMutex(); + } + } + + /// + /// + /// + /// + public void AddConversation(IConversationState conversation) + { + try + { + this.mutexEditDic.WaitOne(5000); + this.conversations.Add(conversation.Id, conversation); + } + finally + { + this.mutexEditDic.ReleaseMutex(); + } + + if (conversation.ConversationManager != null && conversation.ConversationManager != this) + { + throw new InvalidOperationException(String.Format("Conversation already has another manager. conversation='{0}'", conversation.Id)); + } + + if (conversation.ConversationManager == null) + { + conversation.ConversationManager = this; + } + } + + /// + /// + /// + /// + public void SetActiveConversation(IConversationState conversation) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("SetActiveConversation('{0}')", conversation.Id)); + + //Close connection for the last conversation, if it is open. + if (this.activeConversation != null && this.activeConversation != conversation) + { + this.Close(this.SessionFactory, this.conversations.Values); + } + this.Open(conversation, this.conversations.Values); + + this.activeConversation = conversation; + + //Ending the paused conversations. + if (this.EndPaused) + { + foreach (IConversationState convItem in this.conversations.Values) + { + if (convItem.IsPaused) + { + convItem.EndConversation(); + } + } + } + } + + /// + /// + /// + [Obsolete("Not used, the active conversation is defined by call 'IConversationManager.SetActiveConversation' on 'IConversationState.StartResumeConversation'")] + public void LoadActiveConversation() + { + //reset this.activeConversation + this.activeConversation = null; + + if (LOG.IsDebugEnabled) LOG.Debug("LoadActiveConversation"); + + HttpCookie activeConveCookie = HttpContext.Current.Request.Cookies[CONVERSATION_COOKIE_ID]; + if (activeConveCookie != null && !String.IsNullOrEmpty(activeConveCookie.Value)) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("LoadActiveConversation: cooking found for current active conversation: [{0}]", activeConveCookie.ToString())); + if (this.conversations.ContainsKey(activeConveCookie.Value)) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("LoadActiveConversation: active conversation found for id: '{0}'", activeConveCookie.Value)); + IConversationState conversation = this.conversations[activeConveCookie.Value]; + if (conversation != null) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("LoadActiveConversation: conversation found: '{0}'", conversation.Id)); + //find root conversation. + IConversationState rootConversation = conversation; + while (rootConversation.ParenteConversation != null) + { + rootConversation = rootConversation.ParenteConversation; + } + rootConversation.StartResumeConversation(); + this.SetActiveConversation(rootConversation); + } + } + else + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("LoadActiveConversation: conversation NOT found for id on the cookie: '{0}'", activeConveCookie.Value)); + HttpContext.Current.Response.Cookies.Remove(CONVERSATION_COOKIE_ID); + } + } + } + + /// + /// + /// + public IConversationState ActiveConversation + { + get + { + return this.activeConversation; + } + } + + private ISessionFactory sessionFactory; + /// + /// + /// + public ISessionFactory SessionFactory + { + get { return sessionFactory; } + set { sessionFactory = value; } + } + + private bool endPaused = false; + + /// + /// + /// + public bool EndPaused + { + get { return endPaused; } + set { endPaused = value; } + } + #endregion + + #region SessionPerConversationScope Members + /// + /// Ends all conversations and Closes all their Session. + /// + public override void Dispose() + { + if (LOG.IsDebugEnabled) LOG.Debug("Dispose. End all Conversations"); + foreach (String conversationId in this.conversations.Keys) + { + this.conversations[conversationId].EndConversation(); + } + + this.Close(this.SessionFactory, this.conversations.Values); + + this.conversations.Clear(); + this.sessionFactory = null; + } + #endregion + + /// + /// Remove conversation. + /// + /// + private void RemoveConversation(IConversationState conversation) + { + try + { + this.mutexEditDic.WaitOne(5000); + if (!this.conversations.Remove(conversation.Id)) + { + throw new InvalidOperationException(String.Format("Conversation '{0}' not exists on this manager", conversation.Id)); + } + } + finally + { + this.mutexEditDic.ReleaseMutex(); + } + } + } +} diff --git a/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationSpringState.cs b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationSpringState.cs new file mode 100644 index 00000000..5c91e414 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/ConversationWA/Imple/WebConversationSpringState.cs @@ -0,0 +1,601 @@ +using System; +using System.Data; +using System.Configuration; +using System.Web; +using System.Web.Security; +using System.Web.UI; +using System.Web.UI.WebControls; +using System.Web.UI.WebControls.WebParts; +using System.Web.UI.HtmlControls; +using Spring.Objects.Factory; +using Common.Logging; +using System.Collections.Generic; +using System.Collections; +using Spring.Data.Common; +using NHibernate; +using Spring.Data.NHibernate.Support; +using Spring.Data.NHibernate; + +namespace Spring.ConversationWA.Imple +{ + /// + /// Implementation of conversation in the infrastructure of Spring. + /// It avoid Circular Dependence. + /// + /// Hailton de Castro + public class WebConversationSpringState : IConversationState, IObjectNameAware + { + private static readonly ILog LOG = LogManager.GetLogger(typeof(WebConversationSpringState)); + + /// + /// Default message for "CONVERSATION ALREADY HAS A PARENT" error. + /// + public static readonly String MSG_CONVERSATION_ALREADY_HAS_PARENT = + "This conversation already has a different parent." + + " Current: '{0}'. Tried: '{1}'"; + + private IDictionary state = new Dictionary(); + + #region IConversationState Members + + private String id; + /// + /// + /// + public string Id + { + get + { + return this.id; + } + set + { + this.id = value; + } + } + + /// + /// + /// + /// + public void EndConversation() + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("End of Conversation '{0}'", this.id)); + + IDictionary springSessionScope = (IDictionary)HttpContext.Current.Session["spring.objects"]; + if (springSessionScope == null) + { + throw new InvalidOperationException("The 'spring session scope' are not located in the key 'spring.objects' of HttpSessionState"); + } + + if (springSessionScope.Contains(this.Id)) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("EndConversation: Id='{0}' Was Found on 'spring session scope'!", this.id)); + springSessionScope.Remove(this.id); + } + else + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("EndConversation: Id='{0}' Was NOT Found on 'spring session scope!", this.id)); + } + + List innerConversationsListTemp = new List(this.InnerConversations); + foreach (IConversationState innerConversationItem in innerConversationsListTemp) + { + innerConversationItem.EndConversation(); + } + + if (this.parenteConversation != null) + { + this.parenteConversation.InnerConversations.Remove(this); + } + + this.ended = true; + } + + private bool ended = false; + /// + /// + /// + public bool Ended + { + get { return ended; } + } + + private int timeOut = 180000; + /// + /// Default 180000. + /// + public int TimeOut + { + get { return timeOut; } + set { timeOut = value; } + } + + private IList innerConversations; + /// + /// + /// + public IList InnerConversations + { + get + { + if (this.innerConversations == null) + { + this.innerConversations = new InnerConversationList(this); + } + return this.innerConversations; + } + } + + private IConversationState parenteConversation; + /// + /// + /// + public IConversationState ParenteConversation + { + get + { + return this.parenteConversation; + } + set + { + if (this.parenteConversation != null && this.parenteConversation != value) + { + throw new InvalidOperationException( + String.Format(MSG_CONVERSATION_ALREADY_HAS_PARENT, this.parenteConversation.Id, value.Id)); + } + if (!this.IsNew) + { + throw new InvalidOperationException( + String.Format("This Conversation is not new." + + " Conversation.Id: '{0}'. Parent Tried: '{1}'", this.Id, value.Id)); + } + //Perhaps the father need not be new, only the child needs to be. + //if (!value.IsNew) + //{ + // throw new InvalidOperationException( + // String.Format("The Parent conversation is not new." + + // " Conversation.Id: '{0}'. Parent Tried: '{1}'", this.Id, value.Id)); + //} + + this.parenteConversation = value; + if (!this.parenteConversation.InnerConversations.Contains(this)) + { + this.parenteConversation.InnerConversations.Add(this); + } + } + } + + private DateTime lastAccess = DateTime.Now; + /// + /// . + /// + public DateTime LastAccess + { + get { return lastAccess; } + set { lastAccess = value; } + } + + private IConversationManager conversationManager; + /// + /// + /// + public IConversationManager ConversationManager + { + get + { + return conversationManager; + } + set + { + conversationManager = value; + if (this.conversationManager.GetConversationById(this.Id) == null) + { + this.conversationManager.AddConversation(this); + } + } + } + + /// + /// + /// + public void StartResumeConversation() + { + this.isNew = false; + this.isPaused = false; + this.lastAccess = DateTime.Now; + + if (this.Ended) + { + throw new InvalidOperationException( + String.Format( + "StartResumeConversation: this conversation is ended." + + " Conversation.Id '{0}'", this.Id)); + } + if (this.ConversationManager != null) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("StartResumeConversation('{0}'): ConversationManager is not null.", this.Id)); + //if this is the root conversation. + if (this.ParenteConversation == null) + { + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("SetActiveConversation('{0}'): ConversationManager is not null.", this.Id)); + //if this is the root conversation. + this.ConversationManager.SetActiveConversation(this); + + //makes SessionHolder to reopen this session + if (this.RootSessionPerConversation != null) + { + ISession session = this.SessionFactory.GetCurrentSession(); + if (this.RootSessionPerConversation != session) + { + //How it is implemented it will never happen, because of the sequence of previous calls: + // ConversationManager.SetActiveConversation -> SessionPerConversationScope.Open + // 'new InvalidOperationException("Participating in existing Hibernate SessionFactory IS NOT ALOWED.")' happen first. + throw new InvalidOperationException( + String.Format( + "StartResumeConversation: this.SessionFactory.GetCurrentSession()" + + " have a different instance than 'RootSessionPerConversation'" + + " from conversation '{0}'", this.Id)); + } + } + } + else + { + this.ParenteConversation.StartResumeConversation(); + } + } + } + + private ISession rootSessionPerConversation; + /// + /// + /// + public ISession RootSessionPerConversation + { + get + { + if (this.ParenteConversation != null) + { + return this.ParenteConversation.RootSessionPerConversation; + } + else + { + return rootSessionPerConversation; + } + } + set + { + if (this.ParenteConversation != null) + { + this.ParenteConversation.RootSessionPerConversation = value; + } + else + { + rootSessionPerConversation = value; + } + } + } + + private ISessionFactory sessionFactory; + /// + /// + /// + public ISessionFactory SessionFactory + { + get { return sessionFactory; } + set { sessionFactory = value; } + } + + IDbProvider dbProvider; + /// + /// + /// + public IDbProvider DbProvider + { + get { return dbProvider; } + set { dbProvider = value; } + } + + private bool isNew = true; + /// + /// + /// + public bool IsNew + { + get { return this.isNew; } + } + + private bool isPaused = true; + /// + /// + /// + public bool IsPaused + { + get { return isPaused; } + } + + /// + /// + /// + public void PauseConversation() + { + this.isPaused = true; + foreach (IConversationState innerConv in this.InnerConversations) + { + innerConv.PauseConversation(); + } + } + #endregion + + #region IObjectNameAware Members + /// + /// . It is used to valddate + /// + public string ObjectName + { + set + { + if (this.id != value) + { + throw new InvalidOperationException(String.Format("Id is different from spring name for this instance.. Currents='{0}', springName='{1}'", this.id, value)); + } + if (LOG.IsDebugEnabled) LOG.Debug(String.Format("Begin of Conversation '{0}'", this.id)); + } + } + + #endregion + + #region IDictionary Members + /// + /// + /// + /// + /// + public void Add(string key, object value) + { + this.state.Add(key, value); + } + + /// + /// + /// + /// + /// + public bool ContainsKey(string key) + { + if (this.state.ContainsKey(key)) + { + return this.state.ContainsKey(key); + } + else if (this.parenteConversation != null) + { + return this.parenteConversation.ContainsKey(key); + } + else + { + return false; + } + } + + /// + /// + /// + public ICollection Keys + { + get + { + return this.state.Keys; + } + } + + /// + /// + /// + /// + /// + public bool Remove(string key) + { + return this.state.Remove(key); + } + + /// + /// + /// + /// + /// + /// + public bool TryGetValue(string key, out object value) + { + if (this.state.TryGetValue(key, out value)) + { + return true; + } + else if (this.parenteConversation != null && this.parenteConversation.TryGetValue(key, out value)) + { + return true; + } + else + { + return false; + } + } + + /// + /// + /// + public ICollection Values + { + get + { + return this.state.Values; + } + } + + /// + /// + /// + /// + /// + public object this[string key] + { + get + { + if (this.state.ContainsKey(key)) + { + return this.state[key]; + } + else if (this.parenteConversation != null) + { + return this.parenteConversation[key]; + } + else + { + return null; + } + } + set + { + this.state[key] = value; + } + } + + #endregion + + #region ICollection> Members + /// + /// + /// + /// + public void Add(KeyValuePair item) + { + this.state.Add(item); + } + + /// + /// + /// + public void Clear() + { + this.state.Clear(); + } + + /// + /// + /// + /// + /// + public bool Contains(KeyValuePair item) + { + if (this.state.Contains(item)) + { + return this.state.Contains(item); + } + else if (this.parenteConversation != null) + { + return this.parenteConversation.Contains(item); + } + else + { + return false; + } + } + + /// + /// + /// + /// + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + this.state.CopyTo(array, arrayIndex); + } + + /// + /// + /// + public int Count + { + get + { + return this.state.Count; + } + } + + /// + /// + /// + public bool IsReadOnly + { + get + { + return this.state.IsReadOnly; + } + } + + /// + /// + /// + /// + /// + public bool Remove(KeyValuePair item) + { + return this.state.Remove(item); + } + + #endregion + + #region IEnumerable> Members + /// + /// + /// + /// + public IEnumerator> GetEnumerator() + { + return this.state.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + /// + /// + /// + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.state.GetEnumerator(); + } + + #endregion + + /// + /// A String representation from conversation. + /// + /// + public override string ToString() + { + String innerConsversationsStr = ""; + + String conector = ""; + foreach (IConversationState convItem in this.InnerConversations) + { + innerConsversationsStr += conector + convItem.ToString(); + } + + return String.Format("{{Id='{0}'; this.ParenteConversation.Id={1}; InnerConversations=[{2}]}}", + this.id, + this.ParenteConversation != null ? this.ParenteConversation.Id : "", + innerConsversationsStr + ); + } + + /// + /// HashCode. + /// + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationScope.cs b/src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationScope.cs new file mode 100644 index 00000000..bdc15430 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationScope.cs @@ -0,0 +1,450 @@ +#region Licence + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using Common.Logging; +using NHibernate; +using Spring.Threading; +using Spring.Transaction.Support; +using Spring.Util; +using System.Data; +using Spring.ConversationWA; +using System.Collections.Generic; + +#endregion + +namespace Spring.Data.NHibernate.Support +{ + /// + ///Based on + /// for dupport to 'session-per-conversation' pattern. + /// + ///Hailton de Castro + public class SessionPerConversationScope : IDisposable + { + #region Fields + + /// + /// The logging instance. + /// + protected readonly ILog log = LogManager.GetLogger(MethodInfo.GetCurrentMethod().DeclaringType); + + private readonly SessionPerConversationScopeSettings settings; + + // Keys into LogicalThreadContext for runtime values. + private readonly string ISOPEN_KEY; + private readonly string OPENER_CONVERSATION_ID_KEY; + + #endregion + + #region Constructor (s) + + /// + /// Initializes a new instance of the class. + /// Uses default values for + /// + public SessionPerConversationScope() + : this(new SessionPerConversationScopeSettings()) + { + // noop + } + + /// + /// Initializes a new instance of the class. + /// + /// Specify the to be set on each session provided by this instance. + /// Specify the flushmode to be applied on each session provided by this instance. + /// + public SessionPerConversationScope(IInterceptor entityInterceptor, FlushMode defaultFlushMode) + : this(new SessionPerConversationScopeSettings(entityInterceptor, defaultFlushMode)) + { + // noop + } + + /// + /// Initializes a new instance of the class. + /// + /// An instance holding the scope configuration + public SessionPerConversationScope(SessionPerConversationScopeSettings settings) + { + log = LogManager.GetLogger(this.GetType()); + this.settings = settings; + + ISOPEN_KEY = UniqueKey.GetInstanceScopedString(this, "IsOpen"); + OPENER_CONVERSATION_ID_KEY = UniqueKey.GetInstanceScopedString(this, "OpenerConversationId"); + } + + #endregion + + #region Properties + + /// + /// Gets the flushmode to be applied on each newly created session. + /// + /// + /// This property defaults to to ensure that modifying objects outside the boundaries + /// of a transaction will not be persisted. It is recommended to not change this value but wrap any modifying operation + /// within a transaction. + /// + public FlushMode DefaultFlushMode + { + get { return settings.DefaultFlushMode; } + } + + /// + /// Get or set the configured EntityInterceptor + /// + public IInterceptor EntityInterceptor + { + get + { + return settings.EntityInterceptor; + } + } + + /// + /// Id for conversation that open the Session. + /// + public String OpenerConversationId + { + get + { + return (String)LogicalThreadContext.GetData(OPENER_CONVERSATION_ID_KEY); + } + set + { + LogicalThreadContext.SetData(OPENER_CONVERSATION_ID_KEY, value); + } + } + + /// + /// Gets a flag, whether this scope is in "open" state on the current logical thread. + /// + public bool IsOpen + { + get + { + return (null != LogicalThreadContext.GetData(ISOPEN_KEY)); + } + } + + /// + /// Sets a flag, whether this scope is in "open" state on the current logical thread. + /// + /// + private void SetOpen(bool isOpen) + { + if (isOpen) + { + LogicalThreadContext.SetData(ISOPEN_KEY, ISOPEN_KEY); + } + else + { + LogicalThreadContext.FreeNamedDataSlot(ISOPEN_KEY); + } + } + + #endregion + + #region IDisposable Members + + /// + /// NOOP. + /// + public virtual void Dispose() + { + //no OP + log.Warn("I'm not doing anything"); + } + + #endregion + + #region Methods + + /// + /// Open a new session or reconect the + /// in . + /// Participates in an existing session registed with spring's + /// is not alowed. + /// + /// + /// + /// + /// + /// If there is another conversation with a ISession with opened + /// IDbConnection. + /// If trie to participating in existing Hibernate SessionFactory + /// managed by . + /// + /// + /// + public void Open(IConversationState activeConversation, ICollection allManagedConversation) + { + bool isDebugEnabled = log.IsDebugEnabled; + + if (IsOpen) + { + if (activeConversation.Id != this.OpenerConversationId) + { + throw new InvalidOperationException("There is another conversation with a ISession with opened IDbConnection."); + } + else + { + if (isDebugEnabled) log.Debug(String.Format("SessionPerConversationScope is already open for this conversation: Id:'{0}'.", activeConversation.Id)); + } + } + else + { + if (activeConversation.SessionFactory != null) + { + if (isDebugEnabled) log.Debug(String.Format("activeConversation with 'session-per-conversation': Id:'{0}'.", activeConversation.Id)); + + // single session mode + if (TransactionSynchronizationManager.HasResource(activeConversation.SessionFactory)) + { + // Do not modify the Session: just set the participate flag. + if (isDebugEnabled) log.Debug("Participating in existing Hibernate SessionFactory IS NOT ALOWED."); + throw new InvalidOperationException("Participating in existing Hibernate SessionFactory IS NOT ALOWED."); + } + else + { + if (isDebugEnabled) log.Debug("Opening single Hibernate Session in SessionPerConversationScope"); + TransactionSynchronizationManager.BindResource(activeConversation.SessionFactory, new LazySessionPerConversationHolder(this, activeConversation, allManagedConversation)); + + SetOpen(true); + this.OpenerConversationId = activeConversation.Id; + } + } + else + { + if (isDebugEnabled) log.Debug(String.Format("activeConversation with NO 'session-per-conversation': Id:'{0}'.", activeConversation.Id)); + } + } + } + + /// + /// Close the current view's session and unregisters + /// from spring's . + /// + /// The session factory that IConversationState on use + /// A list of conversations which the session can be closed or disconnected + /// + /// + /// If start/resume a conversation from a + /// IConversationManager when exists a diferent IConversationManager + /// with open ISession registered on TransactionSynchronizationManager + /// + /// If the holder on TransactionSynchronizationManager, is not a LazySessionPerConversationHolder. + /// + /// + public void Close(ISessionFactory sessionFactory, ICollection allManagedConversation) + { + bool isDebugEnabled = log.IsDebugEnabled; + if (isDebugEnabled) log.Debug("Trying to close SessionPerConversationScope"); + + if (IsOpen) + { + try + { + DoClose(sessionFactory, allManagedConversation, isDebugEnabled); + } + finally + { + SetOpen(false); + this.OpenerConversationId = null; + } + } + else + { + if (isDebugEnabled) log.Debug("No open conversation - doing nothing"); + } + } + + private void DoClose(ISessionFactory sessionFactory, ICollection allManagedConversation, bool isLogDebugEnabled) + { + // single session mode + if (isLogDebugEnabled) log.Debug("DoClose: Closing SessionPerConversationScope"); + Object holderObj = TransactionSynchronizationManager.UnbindResource(sessionFactory); + if (holderObj != null) + { + if (holderObj is LazySessionPerConversationHolder) + { + LazySessionPerConversationHolder holder = (LazySessionPerConversationHolder)holderObj; + if (holder.Owner == this) + { + holder.CloseAll(); + } + else + { + throw new InvalidOperationException( + "Can not close session beacause 'holder owner' is not 'this'." + + " You are trying to start/resume a conversation from a" + + " IConversationManager when exists a diferent IConversationManager " + + " with open ISession registered on TransactionSynchronizationManager."); + } + } + else + { + throw new InvalidOperationException("Can not close session beacause holder, on TransactionSynchronizationManager, is not a LazySessionPerConversationHolder."); + } + } + else + { + if (isLogDebugEnabled) log.Warn("DoClose: TransactionSynchronizationManager.UnbindResource(sessionFactory) has no SessionHolder. Should I throw error?"); + } + } + + private void DoOpenSession(IConversationState conversation) + { + lock (this) + { + ISession session = null; + if (conversation.RootSessionPerConversation == null) + { + //new session + session = ( + (EntityInterceptor != null) + ? conversation.SessionFactory.OpenSession(EntityInterceptor) + : conversation.SessionFactory.OpenSession() + ); + conversation.RootSessionPerConversation = session; + } + else + { + //reconnect existing one. + if (conversation.DbProvider != null) + { + if (log.IsDebugEnabled) log.Debug(String.Format("DoOpenSession: Conversation has a DbProvider: Id='{0}'", conversation.Id)); + if (!conversation.RootSessionPerConversation.IsConnected) + { + if (log.IsDebugEnabled) log.Debug(String.Format("DoOpenSession: Conversation is not Connected: Id='{0}'", conversation.Id)); + + IDbConnection connection = conversation.DbProvider.CreateConnection(); + connection.Open(); + + conversation.RootSessionPerConversation.Reconnect(connection); + } + else + { + if (log.IsDebugEnabled) log.Debug(String.Format("DoOpenSession: Conversation is already Connected: Id='{0}'", conversation.Id)); + } + } + else + { + if (log.IsDebugEnabled) log.Debug(String.Format("DoOpenSession: Conversation has NO DbProvider: Id='{0}'", conversation.Id)); + conversation.RootSessionPerConversation.Reconnect(); + } + session = conversation.RootSessionPerConversation; + } + session.FlushMode = DefaultFlushMode; + } + } + + #endregion + + #region LazySessionPerConversationHolder utility class + + + /// + /// This sessionHolder creates a session for the active conversation only if it is + /// needed (. + /// + /// + /// Although a NHibernateSession deferes creation of db-connections until they are really + /// needed, instantiation a session is imho still more expensive than this LazySessionHolder. (EE) + /// + private class LazySessionPerConversationHolder : SessionHolder + { + private readonly ILog log = LogManager.GetLogger(typeof(LazySessionPerConversationHolder)); + private SessionPerConversationScope owner; + public SessionPerConversationScope Owner + { + get { return owner; } + } + IConversationState activeConversation; + ICollection allManagedConversation; + + /// + /// Initialize a new instance. + /// + public LazySessionPerConversationHolder(SessionPerConversationScope owner, IConversationState activeConversation, ICollection allManagedConversation) + { + if (log.IsDebugEnabled) log.Debug("Created LazyReconnectableSessionHolder"); + this.owner = owner; + this.activeConversation = activeConversation; + this.allManagedConversation = allManagedConversation; + } + + /// + /// Create a new session on demand + /// + protected override void EnsureInitialized() + { + if (this.activeConversation.RootSessionPerConversation == null || !this.activeConversation.RootSessionPerConversation.IsConnected) + { + if (log.IsDebugEnabled) log.Debug("EnsureInitialized: 'session-per-conversation' instance requested - opening new session"); + owner.DoOpenSession(this.activeConversation); + AddSession(this.activeConversation.RootSessionPerConversation); + } + } + + public void CloseAll() + { + foreach (IConversationState conversation in this.allManagedConversation) + { + this.CloseConversation(conversation); + } + owner = null; + this.activeConversation = null; + this.allManagedConversation = null; + + if (log.IsDebugEnabled) log.Debug("CloseAll LazySessionPerConversationHolder"); + } + + private void CloseConversation(IConversationState conversation) + { + if (log.IsDebugEnabled) log.Debug(String.Format("CloseConversation: Id='{0}'", conversation.Id)); + if (conversation.RootSessionPerConversation != null) + { + ISession tmpSession = conversation.RootSessionPerConversation; + if (conversation.Ended) + { + SessionFactoryUtils.CloseSession(tmpSession); + conversation.RootSessionPerConversation = null; + } + else + { + if (tmpSession.IsConnected) + tmpSession.Disconnect(); + } + RemoveSession(tmpSession); + } + if (log.IsDebugEnabled) log.Debug("Closed LazySessionPerConversationHolder"); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationSettings.cs b/src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationSettings.cs new file mode 100644 index 00000000..68a6f889 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/Data/NHibernate/Support/SessionPerConversationSettings.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NHibernate; +using Spring.Util; + +namespace Spring.Data.NHibernate.Support +{ + /// + /// Setting for + /// + /// Hailton de Castro + public class SessionPerConversationScopeSettings + { + /// + /// Default value for property. + /// + public static readonly FlushMode FLUSHMODE_DEFAULT = FlushMode.Never; + + private IInterceptor entityInterceptor; + private FlushMode defaultFlushMode; + + /// + /// Initialize a new instance of with default values. + /// + /// + /// Calling this constructor from your derived class leaves + /// uninitialized. See for more. + /// + public SessionPerConversationScopeSettings() + { + this.entityInterceptor = null; + this.defaultFlushMode = FLUSHMODE_DEFAULT; + } + + /// + /// Initialize a new instance of with the given values and references. + /// + /// + /// Specify the to be set on each session provided by the instance. + /// + /// + /// Specify the flushmode to be applied on each session provided by the instance. + /// + /// + /// Calling this constructor marks all properties initialized. + /// + public SessionPerConversationScopeSettings(IInterceptor entityInterceptor, FlushMode defaultFlushMode) + { + this.entityInterceptor = entityInterceptor; + this.defaultFlushMode = defaultFlushMode; + } + + #region Properties + + /// + /// Gets the configured instance to be used. + /// + /// + /// + public virtual IInterceptor EntityInterceptor + { + get + { + return entityInterceptor; + } + set + { + entityInterceptor = value; + } + } + + /// + /// Gets or Sets the flushmode to be applied on each newly created session. + /// + /// + /// This property defaults to to ensure that modifying objects outside the boundaries + /// of a transaction will not be persisted. It is recommended to not change this value but wrap any modifying operation + /// within a transaction. + /// + public FlushMode DefaultFlushMode + { + get { return defaultFlushMode; } + set { defaultFlushMode = value; } + } + + #endregion + + /// + /// Override this method to resolve an instance according to your chosen strategy. + /// + protected virtual IInterceptor ResolveEntityInterceptor() + { + return null; + } + } +} diff --git a/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2008.csproj b/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2008.csproj new file mode 100644 index 00000000..1fca81aa --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2008.csproj @@ -0,0 +1,103 @@ + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {64400FF8-2E9F-4809-B5F4-0C7EB8ABFF87} + Library + Properties + Spring + Spring.ConversationWA.NH32 + + + 2.0 + v2.0 + + + + + + + true + full + false + ..\..\..\build\VS.Net.2008\Spring.ConversationWA.NH32\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\Spring.ConversationWA.xml + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + False + ..\..\..\lib\NHibernate32\net\3.5\Iesi.Collections.dll + + + False + ..\..\..\lib\NHibernate32\net\3.5\NHibernate.dll + + + + + + + + + CommonAssemblyInfo.cs + + + + + + + + + + + + + + nant.xsd + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2008 + + + {1C8E0481-A70D-445E-AB4D-4A963CF7DC83} + Spring.Data.NHibernate32.2008 + + + {AE00E5AB-C39A-436F-86D2-33BFE33E2E40} + Spring.Data.2008 + + + + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2010.csproj b/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2010.csproj new file mode 100644 index 00000000..767d94eb --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.2010.csproj @@ -0,0 +1,102 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {64400FF8-2E9F-4809-B5F4-0C7EB8ABFF87} + Library + Properties + Spring + Spring.ConversationWA.NH32 + + + 3.5 + v4.0 + + + + + true + full + false + ..\..\..\build\VS.Net.2010\Spring.ConversationWA.NH32\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\Spring.ConversationWA.xml + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + False + ..\..\..\lib\NHibernate32\net\3.5\Iesi.Collections.dll + + + False + ..\..\..\lib\NHibernate32\net\3.5\NHibernate.dll + + + + + + + + + CommonAssemblyInfo.cs + + + + + + + + + + + + + + nant.xsd + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2010 + + + {1C8E0481-A70D-445E-AB4D-4A963CF7DC83} + Spring.Data.NHibernate32.2010 + + + {AE00E5AB-C39A-436F-86D2-33BFE33E2E40} + Spring.Data.2010 + + + + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.build b/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.build new file mode 100644 index 00000000..b192fd2f --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/Spring.ConversationWA.NH32.build @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Spring/Spring.ConversationWA.NH32/app.config b/src/Spring/Spring.ConversationWA.NH32/app.config new file mode 100644 index 00000000..73859b00 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH32/app.config @@ -0,0 +1,3 @@ + + + diff --git a/src/Spring/Spring.ConversationWA.NH33/AssemblyInfo.cs b/src/Spring/Spring.ConversationWA.NH33/AssemblyInfo.cs new file mode 100644 index 00000000..d2710eea --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: AssemblyTitle("Spring.ConversationWA.NH33. NHibernate 3.3 support.")] +[assembly: AssemblyDescription("Interfaces and classes that provide 'Conversation Workaround' support in Spring.Net")] \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH33/ConversationWA/HttpModule/dontdelete.txt b/src/Spring/Spring.ConversationWA.NH33/ConversationWA/HttpModule/dontdelete.txt new file mode 100644 index 00000000..55d0ec6d --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/ConversationWA/HttpModule/dontdelete.txt @@ -0,0 +1 @@ +sources are linked here from Spring.ConversationWA.NH32 \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH33/ConversationWA/Imple/dontdelete.txt b/src/Spring/Spring.ConversationWA.NH33/ConversationWA/Imple/dontdelete.txt new file mode 100644 index 00000000..55d0ec6d --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/ConversationWA/Imple/dontdelete.txt @@ -0,0 +1 @@ +sources are linked here from Spring.ConversationWA.NH32 \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH33/ConversationWA/dontdelete.txt b/src/Spring/Spring.ConversationWA.NH33/ConversationWA/dontdelete.txt new file mode 100644 index 00000000..55d0ec6d --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/ConversationWA/dontdelete.txt @@ -0,0 +1 @@ +sources are linked here from Spring.ConversationWA.NH32 \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH33/Data/NHibernate/Support/dontdelete.txt b/src/Spring/Spring.ConversationWA.NH33/Data/NHibernate/Support/dontdelete.txt new file mode 100644 index 00000000..55d0ec6d --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/Data/NHibernate/Support/dontdelete.txt @@ -0,0 +1 @@ +sources are linked here from Spring.ConversationWA.NH32 \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2008.csproj b/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2008.csproj new file mode 100644 index 00000000..42d460d1 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2008.csproj @@ -0,0 +1,112 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {CF375928-B6D5-485C-B04D-2BC41D9DBF1E} + Library + Properties + Spring + Spring.ConversationWA.NH33 + v3.0 + 512 + + + + + true + full + false + ..\..\..\build\VS.Net.2008\Spring.ConversationWA.NH33\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\Spring.ConversationWA.NH33.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + False + ..\..\..\lib\NHibernate33\net\3.5\Iesi.Collections.dll + + + False + ..\..\..\lib\NHibernate33\net\3.5\NHibernate.dll + + + + + + + + + CommonAssemblyInfo.cs + + + Conversation\HttpModule\ConversationModule.cs + + + Conversation\IConversationManager.cs + + + Conversation\IConversationState.cs + + + Conversation\Imple\InnerConversationList.cs + + + Conversation\Imple\WebConversationManager.cs + + + Conversation\Imple\WebConversationSpringState.cs + + + Data\NHibernate\Support\SessionPerConversationScope.cs + + + Data\NHibernate\Support\SessionPerConversationSettings.cs + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2008 + + + {67EA5988-C54E-4348-BFFB-E4A61F26143C} + Spring.Data.NHibernate33.2008 + + + {AE00E5AB-C39A-436F-86D2-33BFE33E2E40} + Spring.Data.2008 + + + + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2010.csproj b/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2010.csproj new file mode 100644 index 00000000..9c2392eb --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.2010.csproj @@ -0,0 +1,114 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {CF375928-B6D5-485C-B04D-2BC41D9DBF1E} + Library + Properties + Spring + Spring.ConversationWA.NH33 + v4.0 + 512 + + + 3.5 + + + + true + full + false + ..\..\..\build\VS.Net.2010\Spring.ConversationWA.NH33\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\Spring.ConversationWA.NH33.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + False + ..\..\..\lib\NHibernate33\net\3.5\Iesi.Collections.dll + + + False + ..\..\..\lib\NHibernate33\net\3.5\NHibernate.dll + + + + + + + + + CommonAssemblyInfo.cs + + + Conversation\HttpModule\ConversationModule.cs + + + Conversation\IConversationManager.cs + + + Conversation\IConversationState.cs + + + Conversation\Imple\InnerConversationList.cs + + + Conversation\Imple\WebConversationManager.cs + + + Conversation\Imple\WebConversationSpringState.cs + + + Data\NHibernate\Support\SessionPerConversationScope.cs + + + Data\NHibernate\Support\SessionPerConversationSettings.cs + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2010 + + + {67EA5988-C54E-4348-BFFB-E4A61F26143C} + Spring.Data.NHibernate33.2010 + + + {AE00E5AB-C39A-436F-86D2-33BFE33E2E40} + Spring.Data.2010 + + + + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.build b/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.build new file mode 100644 index 00000000..bb78c171 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/Spring.ConversationWA.NH33.build @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Spring/Spring.ConversationWA.NH33/app.config b/src/Spring/Spring.ConversationWA.NH33/app.config new file mode 100644 index 00000000..73859b00 --- /dev/null +++ b/src/Spring/Spring.ConversationWA.NH33/app.config @@ -0,0 +1,3 @@ + + +