/*********************************************************************/
/* */
/* 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
}
}