/*********************************************************************/ /* */ /* AgentService Framework */ /* www.agentservice.it */ /* */ /* Copyright (C) 2007 l.i.d.o. - DIST, University of Genoa, Italy */ /* GNU Lesser General Public License */ /* */ /* This library is free software; you can redistribute it and/or */ /* modify it under the terms of the GNU Lesser General Public */ /* License as published by the Free Software Foundation, */ /* version 2.1 of the License. */ /* */ /* This library is distributed in the hope that it will be useful, */ /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU */ /* Lesser General Public License for more details. */ /* */ /* You should have received a copy of the GNU Lesser General Public */ /* License along with this library; if not, write to the */ /* Free Software Foundation, Inc., 59 Temple Place - Suite 330, */ /* Boston, MA 02111-1307, USA. */ /* */ /*********************************************************************/ using System; using System.Runtime.Serialization; using AgentService.Agent; using AgentService.Ontology; using AgentService.Platform.AMS; using AgentService.Platform.MTS; namespace AgentService.Agent.Runtime { /// /// Enum ConversationProtocol. Defines the interaction protocol used to /// open and to close conversations /// [Serializable] public enum ConversationProtocol { /// /// Proposes a conversation identifier /// Propose, /// /// Replies to a conversation request /// Reply, /// /// Denies the conversation request /// Deny, /// /// Used to request the end of a conversation /// Close, /// /// Used when one of the peers of the conversation /// chnages the status of the conversation object /// of the other peer /// Signal } /// /// Class Conversation. Default implementation of the IConversation interface /// [Serializable()] [DataContract] public class Conversation: IConversation { /// /// Item name used to transfer the status code item. /// protected static readonly string StatusCodeLabel = "code"; /// /// agent communication language for agent service conversation /// public static readonly string Acl = "ags:conversation"; /// /// reference to the client of the messaging system. /// private DefaultMsgClient msgClient; /// /// Owner's aid of the conversation object /// private AID owner; //Added by Andrea Passadore (20/03/2007): it is used in the new costructors managing the ontologies. /// /// Owner's aid of the conversation object /// [DataMember] public AID Owner { get { return this.owner; } set { owner = value; } } /// /// AID of the other agent that participates in the conversation /// private AID peer; /// /// Gets the AID of the other agent that participates in the conversation /// [DataMember] public AID Peer { get { return this.peer; } set { peer = value; } } /// /// state of the conversation /// internal ConversationState state; /// /// Gets the state of the conversation /// [DataMember] public ConversationState State { get { return this.state; } set { state = value; } } /// /// conversation status code /// internal ConversationStatusCode statusCode = ConversationStatusCode.Ok; /// /// Gets the status code of the conversation /// [DataMember] public ConversationStatusCode StatusCode { get { return this.statusCode; } set { statusCode = value; } } /// /// Unique conversation identifier /// private string convId; /// /// Gets the conversation identifier /// public string ConvId { get { return this.convId; } } /// /// Flag used only for optimization, if true /// the conversation has been removed from the /// conversation collection of the message client /// and we do not need to call RemoveConversation /// again /// [DataMember] private bool bRemoved = false; /// /// If true conversation object executes the thread /// to contract for the conversation identifier /// [DataMember] private bool bRunHandShake = true; /// /// Message body of the incoming conversation /// [DataMember] private ConversationBody body; /// /// Default envelope for all the messages belonging /// to this conversation /// [DataMember] private MessageEnvelope envelope; /// /// Attempts counter /// [DataMember] private int attempts; /// /// Number of max attempts used by the conversation /// object to complete the handshake process /// private int maxAttempts = 5; /// /// Gets, sets the number of max attempts to complete /// the handshake process /// [DataMember] public int MaxAttempts { get { return this.maxAttempts; } set { if (value > 0) { this.maxAttempts = value; } } } //Added by Andrea Passadore (20/03/2007): it manages ontological conversation. /// /// Constructs a Conversation object to open a channel with a given agent /// by using the given message client. Used by the message client when from /// whithin a behaviour a new conversation is requested. /// /// reference to the message client /// aid of the agent holding the conversation /// aid of the agent to talk with /// the ontology descriptor internal Conversation(IMsgClient msgClient, AID owner, AID peer, OntologyTypeDescriptor ontology) { if (msgClient == null) throw new ArgumentNullException("msgClient", "null reference to the message client?"); this.msgClient = (DefaultMsgClient) msgClient; if (owner == null) throw new ArgumentNullException("owner", "null aid?"); this.owner = owner; if (peer == null) throw new ArgumentNullException("peer", "null aid?"); this.peer = peer; this.state = ConversationState.Requesting; this.attempts = 0; this.convId = Guid.NewGuid().ToString(); ConversationBody body = new ConversationBody(this.convId, this.owner); body.Ontology = ontology; AgentMessage request = new AgentMessage(Conversation.Acl, body, this.peer); request.ConversationId = "ServiceMessage"; this.msgClient.SendMessage(request); System.Threading.Thread convThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.handleHandShake)); convThread.Name = String.Format("{0}#conv:handshake", System.Threading.Thread.CurrentThread.Name); convThread.Priority = System.Threading.ThreadPriority.Highest; convThread.Start(); } //Modded by Andrea Passadore (20/03/2007): the code has been moved in Conversation(DefaultMsgClient msgClient, AID owner, AID peer, OntologyTypeDescriptor ontology) /// /// Constructs a Conversation object to open a channel with a given agent /// by using the given message client. Used by the message client when from /// whithin a behaviour a new conversation is requested. /// /// reference to the message client /// aid of the agent holding the conversation /// aid of the agent to talk with internal Conversation(IMsgClient msgClient, AID owner, AID peer) : this(msgClient, owner, peer, new OntologyTypeDescriptor()) { } //Added by Andrea Passadore (20/03/2007): it manages ontological conversation. /// /// Constructs a Conversation object to open a channel with a given agent /// by using the given message client. Used by the message client when an /// incoming conversation request is in the agent message queue. /// /// reference to the message client /// message containing an incoming conversation request /// the ontology descriptor internal Conversation(IMsgClient msgClient, AgentMessage message, OntologyTypeDescriptor ontology) { if (msgClient == null) throw new ArgumentNullException("msgClient", "null reference to the message client?"); this.msgClient = (DefaultMsgClient) msgClient; if (message == null) throw new ArgumentNullException("message", "null message?"); this.owner = message.Envelope.To[0]; this.peer = message.Envelope.From; this.state = ConversationState.Incoming; this.body = message.Body as ConversationBody; this.attempts = 0; } //Modded by Andrea Passadore (20/03/2007): the code has been moved in Conversation(DefaultMsgClient msgClient, AgentMessage message, OntologyTypeDescriptor ontology) /// /// Constructs a Conversation object to open a channel with a given agent /// by using the given message client. Used by the message client when an /// incoming conversation request is in the agent message queue. /// /// reference to the message client /// message containing an incoming conversation request internal Conversation(IMsgClient msgClient, AgentMessage message) : this(msgClient, message, new OntologyTypeDescriptor()) { } #region IConversation Members /// /// Waits for the acknowledge for a limited period of time. /// /// Period of time to wait for the acknowledge, if the /// time span is equal to zero, the method will wait till the conversation is /// accepted or rejected /// true if the acknowledge for a message has been received. public bool WaitForAck(TimeSpan timeout) { // TODO: Add Conversation.WaitForAck implementation int total = timeout.Milliseconds; int step = total / 10; bool bRunning = true; while (bRunning) { switch(this.state) { case ConversationState.Accepted: case ConversationState.Active: return true; case ConversationState.Rejected: case ConversationState.Closed: return false; case ConversationState.Closing: return true; default: { if (total > 0) bRunning = (step-- > 0); } break; } System.Threading.Thread.Sleep(0); } return ((this.state == ConversationState.Accepted) || (this.state == ConversationState.Active)); } /// /// Accepts the incoming conversation that is defined by this object. /// public void Accept() { if (this.state == ConversationState.Incoming) { this.state = ConversationState.Accepted; /* int originalId = this.body.OriginalId; int tempId = 0; bool bContinue = true; do { if (this.attempts < this.maxAttempts) { int alternative = body.AlternativeId; int candidate = body.CandidateId; int proposal = alternative; // System.Console.WriteLine(String.Format("[{3}:{4}] <-- [{0}::{1}::{2}]", body.OriginalId, candidate, alternative, this.owner.Name, this.attempts)); if (alternative == 0) { proposal = candidate; } // we have to check first if there is a match // between the CandidateId and AlternativeId // in the message, if so ok the conversation // has been accepted, if not we have to evaluate // the AlternativeId to see if is suitable for us if (candidate == alternative) { // match occurred!! conversation accepted // if the two are equal it means that we have // already sent it, and we have the id into // the temporary conversation list. So we promote // the id into the active conversations list this.msgClient.PromoteConversationId(alternative); this.state = ConversationState.Accepted; this.convId = alternative; bContinue = false; } else { // for sure, here the previously generated tempId was not // suitable for the peer or is 0. anyways we try to remove // it from the temporary conversations list int oldTempId = tempId; tempId = this.msgClient.GetNewConversationId(this.Peer, proposal); if (oldTempId > 0) this.msgClient.RemoveTempConversationId(oldTempId); body.AlternativeId = tempId; body.Verb = ConversationProtocol.Reply; AgentMessage reply = new AgentMessage(Conversation.Acl,body,this.peer); reply.ConversationId = (int) MessageType.ServiceMessage; this.msgClient.SendMessage(reply); // System.Console.WriteLine(String.Format("[{3}:{4}] --> [{0}::{1}::{2}]", body.OriginalId, body.CandidateId, tempId, this.owner.Name, this.attempts)); if (tempId == proposal) { this.state = ConversationState.Accepted; this.convId = tempId; bContinue = false; this.msgClient.PromoteConversationId(proposal); } else { System.Threading.Thread.Sleep(500); AgentMessageCollection messages = this.msgClient.GetAllMessages(false); foreach(AgentMessage msg in messages) { // if the message is a service message and it comes from the peer agent.... if ((msg.ConversationId == (int) MessageType.ServiceMessage) && (msg.Envelope.From.Equals(this.peer))) { // does it speaks my language? ConversationBody tempBody = msg.Body as ConversationBody; if (tempBody != null) { // ..yes if ((this.body.OriginalId == originalId) && (this.body.Initiator.Equals(this.owner) == false)) { // it is a conversation message bounded to // this conversation object this.msgClient.RemoveMessage(msg); // if it replies me with a new conversation id... if (tempBody.Verb == ConversationProtocol.Reply) { this.body = tempBody; } // if the peer has decided to deny the conversation // (i.e. too many attempts to find a suitable id) else if ((tempBody.Verb == ConversationProtocol.Deny) || (tempBody.Verb == ConversationProtocol.Close)) { this.state = ConversationState.Closed; this.msgClient.RemoveConversation(this); bContinue = false; } } } } } } } } else { body.AlternativeId = tempId; body.Verb = ConversationProtocol.Deny; AgentMessage reply = new AgentMessage(Conversation.Acl,body,this.peer); reply.ConversationId = (int) MessageType.ServiceMessage; this.msgClient.SendMessage(reply); this.state = ConversationState.Closed; this.msgClient.RemoveConversation(this); this.bRemoved = true; System.Console.WriteLine("{0}#conv:handshake conv denied", this.owner.Name); bContinue = false; } this.attempts++; } while (bContinue);*/ } else { this.state = ConversationState.Invalid; } } /// /// Rejects the incoming conversation that is defined by this object. /// public void Reject() { this.Reject(ConversationStatusCode.Other); } /// /// Rejects the incoming conversation represented by this instance /// and specifies additional information for the rejection. /// /// additional informatio for the rejection public void Reject(ConversationStatusCode code) { if (this.state == ConversationState.Incoming) { this.body.Verb = ConversationProtocol.Deny; this.body.Add(Conversation.StatusCodeLabel, code); this.state = ConversationState.Rejected; this.msgClient.RemoveConversation(this); this.bRemoved = true; AgentMessage msg = new AgentMessage(Conversation.Acl, body, this.peer); msg.ConversationId = "ServiceMessage"; this.msgClient.SendMessage(msg); } else { this.state = ConversationState.Invalid; // TODO: throw exception? } } /// /// Changes the status of the conversation on the peer side. /// /// conversation status code public void Signal(ConversationStatusCode code) { if (this.state == ConversationState.Active) { ConversationBody convBody = new ConversationBody(this.convId, this.owner, ConversationProtocol.Signal); convBody.Verb = ConversationProtocol.Signal; convBody.Add(Conversation.StatusCodeLabel, code); AgentMessage msg = new AgentMessage(Conversation.Acl, convBody, this.peer); msg.ConversationId = this.convId; this.msgClient.SendMessage(msg); } } /// /// Looks in the agent message queue for messages belonging to this /// conversation object. /// /// true if there are messages belonging to this conversation object public bool HasMessages() { // TODO: Add Conversation.HasMessages implementation if ((this.state == ConversationState.Accepted) || (this.state == ConversationState.Active) || (this.state == ConversationState.Closing)) { // checks for the first message in the queue belonging to // this conversation, if the message is null there no messages // in the queue belonging to this conversation return (this.msgClient.GetMessage(this.convId, false) != null); } else if (this.state == ConversationState.Requesting) { // just do nothing since the conversation has not been // set up yet return false; } else if (this.state == ConversationState.Closed) { if (bRemoved == false) { this.msgClient.RemoveConversation(this); this.bRemoved = true; } this.state = ConversationState.Invalid; return false; } return false; } /// /// Sends a message whithin this conversation object. /// /// content of the message /// true if the message has been properly sent. public bool SendMessage(AgentService.Platform.MTS.MessageBody content) { if ((this.state == ConversationState.Accepted) || (this.state == ConversationState.Active)) { if (this.envelope == null) { this.envelope = new MessageEnvelope(); this.envelope.To.RemoveAll(); this.envelope.To.Add(this.peer); } AgentMessage message = new AgentMessage(this.envelope,content,this.convId); bool bOk = this.msgClient.SendMessage(message); if (bOk == true) { this.state = ConversationState.Active; } return bOk; } else if ( (this.state == ConversationState.Requesting) || (this.state == ConversationState.Incoming) ) { // just do nothing since the conversation has not been // set up yet return false; } else { this.state = ConversationState.Invalid; return false; } } /// /// Receives the first message belonging to this conversation object. /// /// the message received public AgentService.Platform.MTS.AgentMessage ReceiveMessage() { AgentMessage message = null; if ((this.state == ConversationState.Accepted) || (this.state == ConversationState.Active) || (this.state == ConversationState.Closing) ) { message = this.msgClient.GetMessage(this.convId, true); if (message != null) { if (this.state == ConversationState.Accepted) { this.state = ConversationState.Active; } if (this.handleSignal(message.Body) == true) { message = null; } } } else if (this.state != ConversationState.Requesting) { this.state = ConversationState.Invalid; } return message; } /// /// Looks in the agent message queue for messages belonging to this /// conversation object and returns the first of them whitout popping /// it from the queue. /// /// the first message on the agent message queue belonging to this conversation public AgentService.Platform.MTS.AgentMessage PeekMessage() { AgentMessage message = null; if ((this.state == ConversationState.Accepted) || (this.state == ConversationState.Active)) { message = this.msgClient.GetMessage(this.convId,false); if(message != null) { this.state = ConversationState.Active; } if (this.handleSignal(message.Body) == true) { return null; } } else if ( (this.state != ConversationState.Requesting) && (this.state != ConversationState.Requesting) ) { this.state = ConversationState.Invalid; } return message; } /// /// Waits for a message belonging to this conversation object /// /// /// an agent message object, null if the conversation is in /// the Closed or Invalid state /// public AgentService.Platform.MTS.AgentMessage WaitForMessage() { if ((this.state == ConversationState.Accepted) || (this.state == ConversationState.Active)) { return this.doWaitForMessage(); } else if (this.state == ConversationState.Requesting) { do { // go to sleep for one second System.Threading.Thread.Sleep(1500); } while (this.state == ConversationState.Requesting); return this.doWaitForMessage(); } else { this.state = ConversationState.Invalid; return null; } } /// /// Helper for the WaitForMessage method, actually waits for /// an incoming conversation message /// e /// /// an agent message object, null if the conversation is in /// the Closed or Invalid state /// private AgentMessage doWaitForMessage() { AgentMessage message = null; do { if ((this.state == ConversationState.Closed) || (this.state == ConversationState.Invalid)) { if (this.bRemoved == false) { this.msgClient.RemoveConversation(this); this.bRemoved = true; } return null; } AgentMessageCollection messages = this.msgClient.GetAllMessages(this.convId,false); if(messages.Count != 0) { message = messages[0] as AgentMessage; this.msgClient.RemoveMessage(message); if (message != null) { this.handleSignal(message.Body); } this.state = ConversationState.Active; break; } } while (message == null); return message; } /// /// Closes this conversation object. /// public void Close() { this.Close(ConversationStatusCode.Ok); } /// /// Closes the conversation upon /// a specific event described by the /// status code. /// /// conversation status code public void Close(ConversationStatusCode code) { lock (this) { if ((this.state != ConversationState.Closing) && (this.state != ConversationState.Invalid) && (this.state != ConversationState.Rejected)) { ConversationBody closeBody = new ConversationBody(this.convId, this.peer, ConversationProtocol.Close); closeBody.Add(Conversation.StatusCodeLabel, code); AgentMessage message = new AgentMessage(Conversation.Acl, closeBody, this.peer); message.ConversationId = "ServiceMessage"; this.msgClient.SendMessage(message); } } if (this.bRemoved == false) { this.msgClient.CloseConversation(this.convId); this.bRemoved = true; } this.state = ConversationState.Closed; } #endregion #region helpers /// /// Processes the message body in order to /// look for signals. /// /// this method can be used in the /// future for extending the protocol /// /// private bool handleSignal(MessageBody body) { ConversationBody cb = body as ConversationBody; if (cb != null) { if (cb.Verb == ConversationProtocol.Signal) { try { object item = cb[Conversation.StatusCodeLabel]; this.statusCode = (ConversationStatusCode) item; } catch (InvalidCastException ex) { // TODO: Review the specific code set this.statusCode = ConversationStatusCode.InternalError; } } return true; } else { return false; } } /// /// Contracts the conversation identifier to start the communication with the peer /// private void handleHandShake() { //Note if the peer do not reply the cycle is infinite. //int tempId = this.convId; while (this.bRunHandShake) { AgentMessageCollection messages = this.msgClient.GetAllMessages("ServiceMessage", false); foreach (AgentMessage message in messages) { // if the message is a service message and it comes from the peer agent.... if (message.Envelope.From.Equals(this.peer)) { // does it speak my language? ConversationBody body = message.Body as ConversationBody; if (body != null) { // ..yes if ((body.ConvId.Equals(this.convId)) && (body.Initiator.Name.Equals(this.owner.Name))) { this.msgClient.RemoveMessage(message); if (body.Verb == ConversationProtocol.Deny) { lock (this) { this.state = ConversationState.Rejected; this.statusCode = (ConversationStatusCode)body[Conversation.StatusCodeLabel]; this.msgClient.RemoveConversation(this); return; } } else { if (body.Verb == ConversationProtocol.Reply) { lock (this) { this.state = ConversationState.Accepted; return; } } else { if (body.Verb == ConversationProtocol.Close) { this.msgClient.RemoveMessage(message); this.msgClient.RemoveConversation(this); } } } } else { // ..no, just skip } } } } System.Threading.Thread.Sleep(1000); } } /// /// Stops the handleHandShake thread and sets the state of the conversation to Closed /// internal void Stop() { this.bRunHandShake = false; this.state = ConversationState.Closed; } #endregion } }