restemplate/indy/Protocols/IdHL7.pas

1596 lines
46 KiB
Plaintext

{
$Project$
$Workfile$
$Revision$
$DateUTC$
$Id$
This file is part of the Indy (Internet Direct) project, and is offered
under the dual-licensing agreement described on the Indy website.
(http://www.indyproject.org/)
Copyright:
(c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
$Log$
}
{
Rev 1.9 9/30/2004 5:04:18 PM BGooijen
Self was not initialized
Rev 1.8 6/11/2004 9:36:14 AM DSiders
Added "Do not Localize" comments.
Rev 1.7 2004.02.07 5:03:02 PM czhower
.net fixes.
Rev 1.6 2004.02.03 5:43:44 PM czhower
Name changes
Rev 1.5 1/21/2004 2:42:46 PM JPMugaas
InitComponent
Rev 1.4 1/3/2004 12:59:54 PM JPMugaas
These should now compile with Kudzu's change in IdCoreGlobal.
Rev 1.3 4/12/2003 9:21:32 PM GGrieve
give up on Indy10 for the moment
Rev 1.2 10/15/2003 9:53:42 PM GGrieve
DotNet changes
Rev 1.1 23/6/2003 22:33:54 GGrieve
update for indy10 IOHandler model
Rev 1.0 11/13/2002 07:53:58 AM JPMugaas
05/09/2002 Grahame Grieve
Fixed SingleThread Timeout Issues + WaitForConnection
23/01/2002 Grahame Grieve
Fixed for network changes to TIdTCPxxx wrote DUnit testing,
increased assertions change OnMessageReceive,
added VHandled parameter
07/12/2001 Grahame Grieve Various fixes for cmSingleThread mode
05/11/2001 Grahame Grieve Merge into Indy
03/09/2001 Grahame Grieve Prepare for Indy
}
{
==============================================================================
Warning: this code is currently broken in Indy 10.
The extensive changes to the IOHandler architecture mean that the way
this unit works - doing asynchronous IO in a single connection - can
no longer work without causing access violations whenever the socket
is closed
This code needs to be re-written to resolve these issues somehow, but
no clear design has emerged at this point
==============================================================================
}
{
Indy HL7 Minimal Lower Layer Protocol TIdHL7
Original author Grahame Grieve
This code was donated by HL7Connect.com
For more HL7 open source code see
http://www.hl7connect.com/tools
This unit implements support for the Standard HL7 minimal Lower Layer
protocol. For further details, consult the HL7 standard (www.hl7.org).
Before you can use this component, you must set the following properties:
CommunicationMode
Address (if you want to be a client)
Port
isListener
and hook the appropriate events (see below)
This component will operate as either a server or a client depending on
the configuration
}
(*
note: Events are structurally important for this component. However there is
a bug in SyncObjs for Linux under Kylix 1 and 2 where TEvent.WaitFor cannot be
used with timeouts. If you compile your own RTL, then you can fix the routine
like this:
function TEvent.WaitFor(Timeout: LongWord): TWaitResult;
{$IFDEF LINUX}
var ts : TTimeSpec;
begin
ts.tv_sec := timeout div 1000;
ts.tv_nsec := (timeout mod 1000) * 1000000;
if sem_timedwait(FSem, ts) = 0 then
result := wrSignaled
else
result := wrTimeOut;
{$ENDIF}
and then disable this define:
this is a serious issue - unless you fix the RTL, this component does not
function properly on Linux at the present time. This may be fixed in a
future version
*)
{ TODO : use Server.MaxConnections }
unit IdHL7;
interface
{$i IdCompilerDefines.inc}
uses
Classes,
IdBaseComponent,
IdContext,
IdException,
IdGlobal,
IdTCPClient,
IdTCPConnection,
IdTCPServer,
SysUtils;
const
MSG_START = #$0B; {do not localize}
MSG_END = #$1C#$0D; {do not localize}
type
EHL7CommunicationError = class(EIdException)
Protected
FInterfaceName: String;
Public
constructor Create(AnInterfaceName, AMessage: String);
property InterfaceName: String Read FInterfaceName;
end;
THL7CommunicationMode = (cmUnknown, // not valid - default setting must be changed by application
cmAsynchronous, // see comments below for meanings of the other parameters
cmSynchronous,
cmSingleThread);
TSendResponse = (srNone, // internal use only - never returned
srError, // internal use only - never returned
srNoConnection, // you tried to send but there was no connection
srSent, // you asked to send without waiting, and it has been done
srOK, // sent ok, and response returned
srTimeout); // we sent but there was no response (connection will be dropped internally
TIdHL7Status = (isStopped, // not doing anything
isNotConnected, // not Connected (Server state)
isConnecting, // Client is attempting to connect
isWaitReconnect, // Client is in delay loop prior to attempting to connect
isConnected, // connected OK
isUnusable // Not Usable - stop failed
);
const
{ default property values }
DEFAULT_ADDRESS = ''; {do not localize}
DEFAULT_PORT = 0;
DEFAULT_TIMEOUT = 30000;
DEFAULT_RECEIVE_TIMEOUT = 30000;
NULL_IP = '0.0.0.0'; {do not localize}
DEFAULT_CONN_LIMIT = 1;
DEFAULT_RECONNECT_DELAY = 15000;
DEFAULT_COMM_MODE = cmUnknown;
DEFAULT_IS_LISTENER = True;
MILLISECOND_LENGTH = (1 / (24 * 60 * 60 * 1000));
type
// the connection is provided in these events so that applications can obtain information about the
// the peer. It's never OK to write to these connections
TMessageArriveEvent = procedure(ASender: TObject; AConnection: TIdTCPConnection; AMsg: String) of object;
TMessageReceiveEvent = procedure(ASender: TObject; AConnection: TIdTCPConnection; AMsg: String; var VHandled: Boolean; var VReply: String) of object;
TReceiveErrorEvent = procedure(ASender: TObject; AConnection: TIdTCPConnection; AMsg: String; AException: Exception; var VReply: String; var VDropConnection: Boolean) of object;
TIdHL7 = class;
TIdHL7ConnCountEvent = procedure (ASender : TIdHL7; AConnCount : integer) of object;
TIdHL7ClientThread = class(TThread)
Protected
FClient: TIdTCPClient;
FCloseEvent: TIdLocalEvent;
FOwner: TIdHL7;
procedure Execute; Override;
procedure PollStack;
Public
constructor Create(aOwner: TIdHL7);
destructor Destroy; Override;
end;
TIdHL7 = class(TIdBaseComponent)
Protected
FLock: TIdCriticalSection;
FStatus: TIdHL7Status;
FStatusDesc: String;
// these queues hold messages when running in singlethread mode
FMsgQueue: TList;
FHndMsgQueue: TList;
FAddress: String;
FCommunicationMode: THL7CommunicationMode;
FConnectionLimit: Word;
FIPMask: String;
FIPRestriction: String;
FIsListener: Boolean;
FObject: TObject;
FPreStopped: Boolean;
FPort: Word;
FReconnectDelay: LongWord;
FTimeOut: Cardinal;
FReceiveTimeout: LongWord;
FOnConnect: TNotifyEvent;
FOnDisconnect: TNotifyEvent;
FOnConnCountChange : TIdHL7ConnCountEvent;
FOnMessageArrive: TMessageArriveEvent;
FOnReceiveMessage: TMessageReceiveEvent;
FOnReceiveError: TReceiveErrorEvent;
FIsServer: Boolean;
// current connection count (server only) (can only exceed 1 when mode is not
// asynchronous and we are listening)
FConnCount: Integer;
FServer: TIdTCPServer;
// if we are a server, and the mode is not asynchronous, and we are not listening, then
// we will track the current server connection with this, so we can initiate sending on it
FServerConn: TIdTCPConnection;
// A thread exists to connect and receive incoming tcp traffic
FClientThread: TIdHL7ClientThread;
FClient: TIdTCPClient;
// these fields are used for handling message response in synchronous mode
FWaitingForAnswer: Boolean;
FWaitStop: TDateTime;
FMsgReply: String;
FReplyResponse: TSendResponse;
FWaitEvent: TIdLocalEvent;
procedure SetAddress(const AValue: String);
procedure SetConnectionLimit(const AValue: Word);
procedure SetIPMask(const AValue: String);
procedure SetIPRestriction(const AValue: String);
procedure SetPort(const AValue: Word);
procedure SetReconnectDelay(const AValue: LongWord);
procedure SetTimeOut(const AValue: LongWord);
procedure SetCommunicationMode(const AValue: THL7CommunicationMode);
procedure SetIsListener(const AValue: Boolean);
function GetStatus: TIdHL7Status;
function GetStatusDesc: String;
procedure InternalSetStatus(const AStatus: TIdHL7Status; ADesc: String);
procedure CheckServerParameters;
procedure StartServer;
procedure StopServer;
procedure DropServerConnection;
procedure ServerConnect(AContext: TIdContext);
procedure ServerExecute(AContext: TIdContext);
procedure ServerDisconnect(AContext: TIdContext);
procedure CheckClientParameters;
procedure StartClient;
procedure StopClient;
procedure DropClientConnection;
procedure HandleIncoming(const AMsg : String; AConnection: TIdTCPConnection);
function HandleMessage(const AMsg: String; AConn: TIdTCPConnection; var VReply: String): Boolean;
procedure InitComponent; override;
Public
destructor Destroy; Override;
procedure EnforceWaitReplyTimeout;
function Going: Boolean;
// for the app to use to hold any related object
property ObjTag: TObject Read FObject Write FObject;
// status
property Status: TIdHL7Status Read GetStatus;
property StatusDesc: String Read GetStatusDesc;
function Connected: Boolean;
property IsServer: Boolean Read FIsServer;
procedure Start;
procedure PreStop; // call this in advance to start the shut down process. You do not need to call this
procedure Stop;
procedure WaitForConnection(AMaxLength: Integer); // milliseconds
// asynchronous.
function AsynchronousSend(AMsg: String): TSendResponse;
property OnMessageArrive: TMessageArriveEvent Read FOnMessageArrive Write FOnMessageArrive;
// synchronous
function SynchronousSend(AMsg: String; var VReply: String): TSendResponse;
property OnReceiveMessage: TMessageReceiveEvent Read FOnReceiveMessage Write FOnReceiveMessage;
procedure CheckSynchronousSendResult(AResult: TSendResponse; AMsg: String);
// single thread
procedure SendMessage(AMsg: String);
// you can't call SendMessage again without calling GetReply first
function GetReply(var VReply: String): TSendResponse;
function GetMessage(var VMsg: String): TObject; // return nil if no messages
// if you don't call SendReply then no reply will be sent.
procedure SendReply(AMsgHnd: TObject; AReply: String);
Published
// basic properties
property Address: String Read FAddress Write SetAddress; // leave blank and we will be server
property Port: Word Read FPort Write SetPort Default DEFAULT_PORT;
// milliseconds - message timeout - how long we wait for other system to reply
property TimeOut: LongWord Read FTimeOut Write SetTimeOut Default DEFAULT_TIMEOUT;
// milliseconds - message timeout. When running cmSingleThread, how long we wait for the application to process an incoming message before giving up
property ReceiveTimeout: LongWord Read FReceiveTimeout Write FReceiveTimeout Default DEFAULT_RECEIVE_TIMEOUT;
// server properties
property ConnectionLimit: Word Read FConnectionLimit Write SetConnectionLimit Default DEFAULT_CONN_LIMIT; // ignored if isListener is false
property IPRestriction: String Read FIPRestriction Write SetIPRestriction;
property IPMask: String Read FIPMask Write SetIPMask;
// client properties
// milliseconds - how long we wait after losing connection to retry
property ReconnectDelay: LongWord Read FReconnectDelay Write SetReconnectDelay Default DEFAULT_RECONNECT_DELAY;
// message flow
// Set this to one of 4 possibilities:
//
// cmUnknown
// Default at start up. You must set a value before starting
//
// cmAsynchronous
// Send Messages with AsynchronousSend. does not wait for
// remote side to respond before returning
// Receive Messages with OnMessageArrive. Message may
// be response or new message
// The application is responsible for responding to the remote
// application and dropping the link as required
// You must hook the OnMessageArrive Event before setting this mode
// The property IsListener has no meaning in this mode
//
// cmSynchronous
// Send Messages with SynchronousSend. Remote applications response
// will be returned (or timeout). Only use if IsListener is false
// Receive Messages with OnReceiveMessage. Only if IsListener is
// true
// In this mode, the object will wait for a response when sending,
// and expects the application to reply when a message arrives.
// In this mode, the interface can either be the listener or the
// initiator but not both. IsListener controls which one.
// note that OnReceiveMessage must be thread safe if you allow
// more than one connection to a server
//
// cmSingleThread
// Send Messages with SendMessage. Poll for answer using GetReply.
// Only if isListener is false
// Receive Messages using GetMessage. Return a response using
// SendReply. Only if IsListener is true
// This mode is the same as cmSynchronous, but the application is
// assumed to be single threaded. The application must poll to
// find out what is happening rather than being informed using
// an event in a different thread
property CommunicationMode: THL7CommunicationMode Read FCommunicationMode Write SetCommunicationMode Default DEFAULT_COMM_MODE;
// note that IsListener is not related to which end is client. Either end
// may make the connection, and thereafter only one end will be the initiator
// and one end will be the listener. Generally it is recommended that the
// listener be the server. If the client is listening, network conditions
// may lead to a state where the client has a phantom connection and it will
// never find out since it doesn't initiate traffic. In this case, restart
// the interface if there isn't traffic for a period
property IsListener: Boolean Read FIsListener Write SetIsListener Default DEFAULT_IS_LISTENER;
// useful for application
property OnConnect: TNotifyEvent Read FOnConnect Write FOnConnect;
property OnDisconnect: TNotifyEvent Read FOnDisconnect Write FOnDisconnect;
// this is called whenever OnConnect and OnDisconnect are called, and at other times, but only when server
// it will be called after OnConnect and before OnDisconnect
property OnConnCountChange : TIdHL7ConnCountEvent read FOnConnCountChange write FOnConnCountChange;
// this is called when an unhandled exception is generated by the
// hl7 object or the application. It allows the application to
// construct a useful return error, log the exception, and drop the
// connection if it wants
property OnReceiveError: TReceiveErrorEvent Read FOnReceiveError Write FOnReceiveError;
end;
implementation
uses
{$IFDEF USE_VCL_POSIX}
{$IFDEF DARWIN}
CoreServices,
{$ENDIF}
PosixSysSelect,
PosixSysTime,
{$ENDIF}
IdGlobalProtocols,
IdResourceStringsProtocols;
type
TQueuedMessage = class(TIdInterfacedObject)
Private
FEvent: TIdLocalEvent;
FMsg: String;
FTimeOut: LongWord;
FReply: String;
procedure Wait;
Public
constructor Create(aMsg: String; ATimeOut: LongWord);
destructor Destroy; Override;
end;
{ TQueuedMessage }
constructor TQueuedMessage.Create(aMsg: String; ATimeOut: LongWord);
begin
assert(aMsg <> '', 'Attempt to queue an empty message'); {do not localize}
assert(ATimeout <> 0, 'Attempt to queue a message with a 0 timeout'); {do not localize}
inherited Create;
FEvent := TIdLocalEvent.Create(False, False);
FMsg := aMsg;
FTimeOut := ATimeOut;
end;
destructor TQueuedMessage.Destroy;
begin
assert(self <> NIL);
FreeAndNil(FEvent);
inherited;
end;
procedure TQueuedMessage.Wait;
begin
assert(Assigned(Self));
assert(Assigned(FEvent));
FEvent.WaitFor(FTimeOut);
end;
{ EHL7CommunicationError }
constructor EHL7CommunicationError.Create(AnInterfaceName, AMessage: String);
begin
// assert(AInterfaceName <> '', 'Attempt to create an exception for an unnamed interface')
// assert(AMessage <> '', 'Attempt to create an exception with an empty message')
// actually, we do not enforce either of these conditions, though they should both be true,
// since we are already raising an exception
FInterfaceName := AnInterfaceName;
if FInterfaceName <> '' then {do not localize}
begin
inherited Create('[' + AnInterfaceName + '] ' + AMessage)
end
else
begin
inherited Create(AMessage);
end
end;
{ TIdHL7 }
procedure TIdHL7.InitComponent;
begin
inherited;
raise EIdException.create(RSHL7Broken); {do not localize}
// partly redundant initialization of properties
FIsListener := DEFAULT_IS_LISTENER;
FCommunicationMode := DEFAULT_COMM_MODE;
FTimeOut := DEFAULT_TIMEOUT;
FReconnectDelay := DEFAULT_RECONNECT_DELAY;
FReceiveTimeout := DEFAULT_RECEIVE_TIMEOUT;
FConnectionLimit := DEFAULT_CONN_LIMIT;
FIPMask := NULL_IP;
FIPRestriction := NULL_IP;
FAddress := DEFAULT_ADDRESS;
FPort := DEFAULT_PORT;
FOnReceiveMessage := NIL;
FOnConnect := NIL;
FOnDisconnect := NIL;
FObject := NIL;
// initialise status
FStatus := IsStopped;
FStatusDesc := RSHL7StatusStopped;
// build internal infrastructure
Flock := TIdCriticalSection.Create;
FConnCount := 0;
FServer := NIL;
FServerConn := NIL;
FClientThread := NIL;
FClient := NIL;
FMsgQueue := TList.Create;
FHndMsgQueue := TList.Create;
FWaitingForAnswer := False;
FMsgReply := ''; {do not localize}
FReplyResponse := srNone;
FWaitEvent := TIdLocalEvent.Create(False, False);
end;
destructor TIdHL7.Destroy;
begin
assert(Assigned(Self));
try
if Going then
begin
Stop;
end;
finally
FreeAndNil(FMsgQueue);
FreeAndNil(FHndMsgQueue);
FreeAndNil(FWaitEvent);
FreeAndNil(FLock);
inherited;
end;
end;
{==========================================================
Property Servers
==========================================================}
procedure TIdHL7.SetAddress(const AValue: String);
begin
assert(Assigned(Self));
// we don't make any assertions about AValue - will be '' if we are a server
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Address'])); {do not localize??}
end;
FAddress := AValue;
end;
procedure TIdHL7.SetConnectionLimit(const AValue: Word);
begin
assert(Assigned(Self));
// no restrictions on AValue
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['ConnectionLimit'])); {do not localize??}
end;
FConnectionLimit := AValue;
end;
procedure TIdHL7.SetIPMask(const AValue: String);
begin
assert(Assigned(Self));
// to do: enforce that AValue is a valid Subnet mask
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['IP Mask'])); {do not localize??}
end;
FIPMask := AValue;
end;
procedure TIdHL7.SetIPRestriction(const AValue: String);
begin
assert(Assigned(Self));
// to do: enforce that AValue is a valid IP address range
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['IP Restriction'])); {do not localize??}
end;
FIPRestriction := AValue;
end;
procedure TIdHL7.SetPort(const AValue: Word);
begin
assert(Assigned(Self));
assert(AValue <> 0, 'Attempt to use Port 0 for HL7 Communications'); {do not localize}
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Port'])); {do not localize}
end;
FPort := AValue;
end;
procedure TIdHL7.SetReconnectDelay(const AValue: LongWord);
begin
assert(Assigned(Self));
// any value for AValue is accepted, although this may not make sense
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Reconnect Delay'])); {do not localize}
end;
FReconnectDelay := AValue;
end;
procedure TIdHL7.SetTimeOut(const AValue: LongWord);
begin
assert(Assigned(Self));
assert(FTimeout > 0, 'Attempt to configure TIdHL7 with a Timeout of 0'); {do not localize}
// we don't fucntion at all if timeout is 0, though there is circumstances where it's not relevent
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Time Out'])); {do not localize??}
end;
FTimeOut := AValue;
end;
procedure TIdHL7.SetCommunicationMode(const AValue: THL7CommunicationMode);
begin
assert(Assigned(Self));
Assert((AValue >= Low(THL7CommunicationMode)) and (AValue <= High(THL7CommunicationMode)), 'Value for TIdHL7.CommunicationMode not in range'); {do not localize}
// only could arise if someone is typecasting?
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['Communication Mode'])); {do not localize}
end;
FCommunicationMode := AValue;
end;
procedure TIdHL7.SetIsListener(const AValue: Boolean);
begin
assert(Assigned(Self));
// AValue isn't checked
if Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWhileWorking, ['IsListener'])); {do not localize}
end;
FIsListener := AValue;
end;
function TIdHL7.GetStatus: TIdHL7Status;
begin
assert(Assigned(Self));
assert(Assigned(FLock));
FLock.Enter;
try
Result := FStatus;
finally
FLock.Leave;
end;
end;
function TIdHL7.Connected: Boolean;
begin
assert(Assigned(Self));
assert(Assigned(FLock));
FLock.Enter;
try
Result := FStatus = IsConnected;
finally
FLock.Leave;
end;
end;
function TIdHL7.GetStatusDesc: String;
begin
assert(Assigned(Self));
assert(Assigned(FLock));
FLock.Enter;
try
Result := FStatusDesc;
finally
FLock.Leave;
end;
end;
procedure TIdHL7.InternalSetStatus(const AStatus: TIdHL7Status; ADesc: String);
begin
assert(Assigned(Self));
Assert((AStatus >= Low(TIdHL7Status)) and (AStatus <= High(TIdHL7Status)), 'Value for TIdHL7.CommunicationMode not in range'); {do not localize}
// ADesc is allowed to be anything at all
assert(Assigned(FLock));
FLock.Enter;
try
FStatus := AStatus;
FStatusDesc := ADesc;
finally
FLock.Leave;
end;
end;
{==========================================================
Application Control
==========================================================}
procedure TIdHL7.Start;
var
LStatus: TIdHL7Status;
begin
assert(Assigned(Self));
LStatus := GetStatus;
if LStatus = IsUnusable then
begin
raise EHL7CommunicationError.Create(Name, RSHL7NotFailedToStop);
end;
if LStatus <> IsStopped then
begin
raise EHL7CommunicationError.Create(Name, RSHL7AlreadyStarted);
end;
if FCommunicationMode = cmUnknown then
begin
raise EHL7CommunicationError.Create(Name, RSHL7ModeNotSet);
end;
if FCommunicationMode = cmAsynchronous then
begin
if not Assigned(FOnMessageArrive) then
begin
raise EHL7CommunicationError.Create(Name, RSHL7NoAsynEvent);
end;
end;
if (FCommunicationMode = cmSynchronous) and IsListener then
begin
if not Assigned(FOnReceiveMessage) then
begin
raise EHL7CommunicationError.Create(Name, RSHL7NoSynEvent);
end;
end;
FIsServer := (FAddress = '');
if FIsServer then
begin
StartServer
end
else
begin
StartClient;
end;
FPreStopped := False;
FWaitingForAnswer := False;
end;
procedure TIdHL7.PreStop;
procedure JolList(l: TList);
var
i: Integer;
begin
for i := 0 to l.Count - 1 do
begin
TQueuedMessage(l[i]).FEvent.SetEvent;
end;
end;
begin
assert(Assigned(Self));
if FCommunicationMode = cmSingleThread then
begin
assert(Assigned(FLock));
assert(Assigned(FMsgQueue));
assert(Assigned(FHndMsgQueue));
FLock.Enter;
try
JolList(FMsgQueue);
JolList(FHndMsgQueue);
finally
FLock.Leave;
end;
end;
FPreStopped := True;
end;
procedure TIdHL7.Stop;
begin
assert(Assigned(Self));
if not Going then
begin
raise EHL7CommunicationError.Create(Name, RSHL7AlreadyStopped);
end;
if not FPreStopped then
begin
PreStop;
IndySleep(10); // give other threads a chance to clean up
end;
if FIsServer then
begin
StopServer
end
else
begin
StopClient;
end;
end;
{==========================================================
Server Connection Maintainance
==========================================================}
procedure TIdHL7.EnforceWaitReplyTimeout;
begin
Stop;
Start;
end;
function TIdHL7.Going: Boolean;
var
LStatus: TIdHL7Status;
begin
assert(Assigned(Self));
LStatus := GetStatus;
Result := (LStatus <> IsStopped) and (LStatus <> IsUnusable);
end;
procedure TIdHL7.WaitForConnection(AMaxLength: Integer);
var
LStopWaiting: TDateTime;
begin
LStopWaiting := Now + (AMaxLength * ((1 / (24 * 60)) / (60 * 1000)));
while not Connected and (LStopWaiting > Now) do
IndySleep(50);
end;
procedure TIdHL7.CheckSynchronousSendResult(AResult: TSendResponse; AMsg: String);
begin
case AResult of
srNone:
raise EHL7CommunicationError.Create(Name,RSHL7ErrInternalsrNone);
srError:
raise EHL7CommunicationError.Create(Name, AMsg);
srNoConnection:
raise EHL7CommunicationError.Create(Name,RSHL7ErrNotConn);
srSent:
// cause this should only be returned asynchronously
raise EHL7CommunicationError.Create(Name,RSHL7ErrInternalsrSent);
srOK:; // all ok
srTimeout:
raise EHL7CommunicationError.Create(Name,RSHL7ErrNoResponse);
else
raise EHL7CommunicationError.Create(Name,RSHL7ErrInternalUnknownVal + IntToStr(Ord(AResult))); {do not localize}
end;
end;
procedure TIdHL7.CheckServerParameters;
begin
assert(Assigned(Self));
if (FCommunicationMode = cmAsynchronous) or not FIsListener then
begin
FConnectionLimit := 1;
end;
if (FPort < 1) then // though we have already ensured that this cannot happen
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7InvalidPort, [FPort]));
end;
end;
procedure TIdHL7.StartServer;
begin
assert(Assigned(Self));
CheckServerParameters;
FServer := TIdTCPServer.Create(NIL);
try
FServer.DefaultPort := FPort;
Fserver.OnConnect := ServerConnect;
FServer.OnExecute := ServerExecute;
FServer.OnDisconnect := ServerDisconnect;
FServer.Active := True;
InternalSetStatus(IsNotConnected, RSHL7StatusNotConnected);
except
on e:
Exception do
begin
InternalSetStatus(IsStopped, IndyFormat(RSHL7StatusFailedToStart, [e.message]));
FreeAndNil(FServer);
raise;
end;
end;
end;
procedure TIdHL7.StopServer;
begin
assert(Assigned(Self));
try
FServer.Active := False;
FreeAndNil(FServer);
InternalSetStatus(IsStopped, RSHL7StatusStopped);
except
on e:
Exception do
begin
// somewhat arbitrary decision: if for some reason we fail to shutdown,
// we will stubbornly refuse to work again.
InternalSetStatus(IsUnusable, IndyFormat(RSHL7StatusFailedToStop, [e.message]));
FServer := NIL;
raise
end;
end;
end;
procedure TIdHL7.ServerConnect(AContext: TIdContext);
var
LNotify : Boolean;
LConnCount : integer;
LValid : Boolean;
begin
assert(Assigned(Self));
assert(Assigned(AContext));
assert(Assigned(FLock));
FLock.Enter;
try
LNotify := FConnCount = 0;
inc(FConnCount);
LConnCount := FConnCount;
// it would be better to stop getting here in the case of an invalid connection
// cause here we drop it - nasty for the client. To be investigated later
LValid := FConnCount <= FConnectionLimit;
if (FConnCount = 1) and (FCommunicationMode <> cmAsynchronous) and not IsListener then
begin
FServerConn := AContext.Connection;
end;
if LNotify then
begin
InternalSetStatus(IsConnected, RSHL7StatusConnected);
end;
finally
FLock.Leave;
end;
if LValid then
begin
if LNotify and Assigned(FOnConnect) then
begin
FOnConnect(self);
end;
if Assigned(FOnConnCountChange) and (FConnectionLimit <> 1) then
begin
FOnConnCountChange(Self, LConnCount);
end;
end
else
begin
// Thread exceeds connection limit
AContext.Connection.Disconnect;
end;
end;
procedure TIdHL7.ServerDisconnect(AContext: TIdContext);
var
LNotify: Boolean;
LConnCount : integer;
begin
assert(Assigned(Self));
assert(Assigned(AContext));
assert(Assigned(FLock));
FLock.Enter;
try
dec(FConnCount);
LNotify := FConnCount = 0;
LConnCount := FConnCount;
if AContext.Connection = FServerConn then
begin
FServerConn := NIL;
end;
if LNotify then
begin
InternalSetStatus(IsNotConnected, RSHL7StatusNotConnected);
end;
finally
FLock.Leave;
end;
if Assigned(FOnConnCountChange) and (FConnectionLimit <> 1) then
begin
FOnConnCountChange(Self, LConnCount);
end;
if LNotify and Assigned(FOnDisconnect) then
begin
FOnDisconnect(self);
end;
end;
procedure TIdHL7.ServerExecute(AContext: TIdContext);
var
s : String;
begin
assert(Assigned(Self));
assert(Assigned(AContext));
try
// 1. prompt the network for content.
AContext.Connection.IOHandler.ReadLn(MSG_START); // throw this content away
if Assigned(AContext.Connection.IOHandler) then
begin
s := AContext.Connection.IOHandler.ReadLn(MSG_END);
if length(s) > 0 then
begin
HandleIncoming(s, AContext.Connection);
end;
end;
except
try
// well, there was some network error. We aren't sure what it
// was, and it doesn't matter for this layer. we're just going
// to make sure that we start again.
// to review: what happens to the error messages?
AContext.Connection.Disconnect;
except
end;
end;
end;
procedure TIdHL7.DropServerConnection;
begin
assert(Assigned(Self));
assert(Assigned(FLock));
FLock.Enter;
try
if Assigned(FServerConn) then
FServerConn.Disconnect;
finally
FLock.Leave;
end;
end;
{==========================================================
Client Connection Maintainance
==========================================================}
procedure TIdHL7.CheckClientParameters;
begin
assert(Assigned(Self));
if (FPort < 1) then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7InvalidPort, [FPort]));
end;
end;
procedure TIdHL7.StartClient;
begin
assert(Assigned(Self));
CheckClientParameters;
FClientThread := TIdHL7ClientThread.Create(self);
InternalSetStatus(isConnecting, RSHL7StatusConnecting);
end;
procedure TIdHL7.StopClient;
var
LFinished: Boolean;
LWaitStop: LongWord;
begin
assert(Assigned(Self));
assert(Assigned(FLock));
FLock.Enter;
try
FClientThread.Terminate;
FClientThread.FClient.Disconnect;
FClientThread.FCloseEvent.SetEvent;
finally
FLock.Leave;
end;
LWaitStop := Ticks + 5000;
repeat
LFinished := (GetStatus = IsStopped);
if not LFinished then
begin
IndySleep(10);
end;
until LFinished or (Ticks > LWaitStop);
if GetStatus <> IsStopped then
begin
// for some reason the client failed to shutdown. We will stubbornly refuse to work again
InternalSetStatus(IsUnusable, IndyFormat(RSHL7StatusFailedToStop, [RSHL7ClientThreadNotStopped]));
end;
end;
procedure TIdHL7.DropClientConnection;
begin
assert(Assigned(Self));
assert(Assigned(FLock));
FLock.Enter;
try
if Assigned(FClientThread) and Assigned(FClientThread.FClient) then
begin
FClientThread.FClient.Disconnect;
end
else
begin
// This may happen validly because both ends are trying to drop the connection simultaineously
end;
finally
FLock.Leave;
end;
end;
{ TIdHL7ClientThread }
constructor TIdHL7ClientThread.Create(aOwner: TIdHL7);
begin
assert(Assigned(AOwner));
FOwner := aOwner;
FCloseEvent := TIdLocalEvent.Create(True, False);
inherited Create(False);
FreeOnTerminate := True;
end;
destructor TIdHL7ClientThread.Destroy;
begin
assert(Assigned(Self));
assert(Assigned(FOwner));
assert(Assigned(FOwner.FLock));
FreeAndNil(FCloseEvent);
try
FOwner.FLock.Enter;
try
FOwner.FClientThread := NIL;
FOwner.InternalSetStatus(isStopped, RSHL7StatusStopped);
finally
FOwner.FLock.Leave;
end;
except
// it's really vaguely possible that the owner
// may be dead before we are. If that is the case, we blow up here.
// who cares.
end;
inherited;
end;
procedure TIdHL7ClientThread.PollStack;
var
LBuffer: String;
begin
assert(Assigned(Self));
LBuffer := '';
repeat
// we don't send here - we just poll the stack for content
// if the application wants to terminate us at this point,
// then it will disconnect the socket and we will get thrown
// out
// we really don't care at all whether the disconnect was clean or ugly
// but we do need to suppress exceptions that come from
// indy otherwise the client thread will terminate
try
FClient.IOHandler.ReadLn(MSG_START); // we toss this content
if Assigned(FClient.IOHandler) then
begin
LBuffer := FClient.IOHandler.ReadLn(MSG_END);
if LBuffer <> '' then
begin
FOwner.HandleIncoming(LBuffer, FClient);
end;
end;
except
try
// well, there was some network error. We aren't sure what it
// was, and it doesn't matter for this layer. we're just going
// to make sure that we start again.
// to review: what happens to the error messages?
FClient.Disconnect;
except
end;
end;
until Terminated or not FClient.Connected;
end;
procedure TIdHL7ClientThread.Execute;
var
LRecTime: TDateTime;
begin
assert(Assigned(Self));
try
FClient := TIdTCPClient.Create(NIL);
try
FClient.Host := FOwner.FAddress;
FClient.Port := FOwner.FPort;
repeat
// try to connect. Try indefinitely but wait Owner.FReconnectDelay
// between attempts. Problems: how long does Connect take?
repeat
FOwner.InternalSetStatus(IsConnecting, rsHL7StatusConnecting);
try
FClient.Connect;
except
on e:
Exception do
begin
LRecTime := Now + ((FOwner.FReconnectDelay / 1000) * {second length} (1 / (24 * 60 * 60)));
//not we can take more liberties with the time and date output because it's only
//for human consumption (probably in a log
FOwner.InternalSetStatus(IsWaitReconnect, IndyFormat(rsHL7StatusReConnect, [DateTimeToStr(LRecTime), e.message])); {do not localize??}
end;
end;
if not Terminated and not FClient.Connected then
begin
FCloseEvent.WaitFor(FOwner.FReconnectDelay);
end;
until Terminated or FClient.Connected;
if Terminated then
begin
exit;
end;
FOwner.FLock.Enter;
try
FOwner.FClient := FClient;
FOwner.InternalSetStatus(IsConnected, rsHL7StatusConnected);
finally
FOwner.FLock.Leave;
end;
if Assigned(FOwner.FOnConnect) then
begin
FOwner.FOnConnect(FOwner);
end;
try
PollStack;
finally
FOwner.FLock.Enter;
try
FOwner.FClient := NIL;
FOwner.InternalSetStatus(IsNotConnected, RSHL7StatusNotConnected);
finally
FOwner.FLock.Leave;
end;
if Assigned(FOwner.FOnDisconnect) then
begin
FOwner.FOnDisconnect(FOwner);
end;
end;
if not Terminated then
begin
// we got disconnected. ReconnectDelay applies.
FCloseEvent.WaitFor(FOwner.FReconnectDelay);
end;
until terminated;
finally
FreeAndNil(FClient);
end;
except
on e:
Exception do
// presumably some comms or indy related exception
// there's not really anyplace good to put this????
end;
end;
{==========================================================
Internal process management
==========================================================}
procedure TIdHL7.HandleIncoming(const AMsg : String; AConnection: TIdTCPConnection);
var
LReply: String;
begin
assert(Assigned(Self));
assert(AMsg <> '', 'Attempt to handle an empty Message'); {do not localize}
assert(Assigned(AConnection));
try
// process any messages in the buffer (may get more than one per packet)
if HandleMessage(AMsg, AConnection, LReply) then
begin
if LReply <> '' then
begin
AConnection.IOHandler.Write(MSG_START + LReply + MSG_END);
end;
end
else
begin
AConnection.Disconnect;
end;
except
// well, we need to suppress the exception, and force a reconnection
// we don't know why an exception has been allowed to propagate back
// to us, it shouldn't be allowed. so what we're going to do, is drop
// the connection so that we force all the network layers on both
// ends to reconnect.
// this is a waste of time if the error came from the application but
// this is not supposed to happen
try
AConnection.Disconnect;
except
// nothing - suppress
end;
end;
end;
function TIdHL7.HandleMessage(const AMsg: String; AConn: TIdTCPConnection; var VReply: String): Boolean;
var
LQueMsg: TQueuedMessage;
LIndex: Integer;
begin
assert(Assigned(Self));
assert(AMsg <> '', 'Attempt to handle an empty Message'); {do not localize}
assert(Assigned(FLock));
VReply := '';
Result := True;
try
case FCommunicationMode of
cmUnknown:
begin
raise EHL7CommunicationError.Create(Name, RSHL7ImpossibleMessage);
end;
cmAsynchronous:
begin
FOnMessageArrive(self, AConn, Amsg);
end;
cmSynchronous, cmSingleThread:
begin
if IsListener then
begin
if FCommunicationMode = cmSynchronous then
begin
Result := False;
FOnReceiveMessage(self, AConn, AMsg, Result, VReply)
end
else
begin
LQueMsg := TQueuedMessage.Create(AMsg, FReceiveTimeout);
LQueMsg._AddRef;
try
FLock.Enter;
try
FMsgQueue.Add(LQueMsg);
finally
FLock.Leave;
end;
LQueMsg.wait;
// no locking. There is potential problems here. To be reviewed
VReply := LQueMsg.FReply;
finally
FLock.Enter;
try
LIndex := FMsgQueue.IndexOf(LQueMsg);
if LIndex > -1 then
FMsgQueue.Delete(LIndex);
finally
FLock.Leave;
end;
LQueMsg._Release;
end;
end
end
else
begin
FLock.Enter;
try
if FWaitingForAnswer then
begin
FWaitingForAnswer := False;
FMsgReply := AMsg;
FReplyResponse := srOK;
if FCommunicationMode = cmSynchronous then
begin
assert(Assigned(FWaitEvent));
FWaitEvent.SetEvent;
end;
end
else
begin
// we could have got here by timing out, but this is quite unlikely,
// since the connection will be dropped in that case. We will report
// this as a spurious message
raise EHL7CommunicationError.Create(Name, RSHL7UnexpectedMessage);
end;
finally
FLock.Leave;
end;
end
end;
else
begin
raise EHL7CommunicationError.Create(Name, RSHL7UnknownMode);
end;
end;
except
on e:
Exception do
if Assigned(FOnReceiveError) then
begin
FOnReceiveError(self, AConn, AMsg, e, VReply, Result)
end
else
begin
Result := False;
end;
end;
end;
{==========================================================
Sending
==========================================================}
// this procedure is not technically thread safe.
// if the connection is disappearing when we are attempting
// to write, we can get transient access violations. Several
// strategies are available to prevent this but they significantly
// increase the scope of the locks, which costs more than it gains
function TIdHL7.AsynchronousSend(AMsg: String): TSendResponse;
begin
assert(Assigned(self));
assert(AMsg <> '', 'Attempt to send an empty message'); {do not localize}
assert(Assigned(FLock));
Result := srNone; // just to suppress the compiler warning
FLock.Enter;
try
if not Going then
begin
raise EHL7CommunicationError.Create(Name, IndyFormat(RSHL7NotWorking, [RSHL7SendMessage]))
end
else if GetStatus <> isConnected then
begin
Result := srNoConnection
end
else
begin
if FIsServer then
begin
if Assigned(FServerConn) then
begin
FServerConn.IOHandler.Write(MSG_START + AMsg + MSG_END);
Result := srSent
end
else
begin
raise EHL7CommunicationError.Create(Name, RSHL7NoConnectionFound);
end
end
else
begin
FClient.IOHandler.Write(MSG_START + AMsg + MSG_END);
Result := srSent
end;
end;
finally
FLock.Leave;
end
end;
function TIdHL7.SynchronousSend(AMsg: String; var VReply: String): TSendResponse;
begin
assert(Assigned(self));
assert(AMsg <> '', 'Attempt to send an empty message'); {do not localize}
assert(Assigned(FLock));
Result := srError;
FLock.Enter;
try
FWaitingForAnswer := True;
FWaitStop := Now + (FTimeOut * MILLISECOND_LENGTH);
FReplyResponse := srTimeout;
FMsgReply := '';
finally
FLock.Leave;
end;
try
Result := AsynchronousSend(AMsg);
if Result = srSent then
begin
assert(Assigned(FWaitEvent));
FWaitEvent.WaitFor(FTimeOut);
end;
finally
FLock.Enter;
try
FWaitingForAnswer := False;
if Result = srSent then
begin
Result := FReplyResponse;
end;
if Result = srTimeout then
begin
if FIsServer then
DropServerConnection
else
DropClientConnection;
end;
VReply := FMsgReply;
finally
FLock.Leave;
end;
end;
end;
procedure TIdHL7.SendMessage(AMsg: String);
begin
assert(Assigned(self));
assert(AMsg <> '', 'Attempt to send an empty message'); {do not localize}
assert(Assigned(FLock));
if FWaitingForAnswer then
raise EHL7CommunicationError.Create(Name, RSHL7WaitForAnswer);
FLock.Enter;
try
FWaitingForAnswer := True;
FWaitStop := Now + (FTimeOut * MILLISECOND_LENGTH);
FMsgReply := '';
FReplyResponse := AsynchronousSend(AMsg);
finally
FLock.Leave;
end;
end;
function TIdHL7.GetReply(var VReply: String): TSendResponse;
begin
assert(Assigned(self));
assert(Assigned(FLock));
FLock.Enter;
try
if FWaitingForAnswer then
begin
if FWaitStop < Now then
begin
Result := srTimeout;
VReply := '';
FWaitingForAnswer := False;
FReplyResponse := srError;
end
else
begin
Result := srNone;
end;
end
else
begin
Result := FReplyResponse;
if Result = srSent then
begin
Result := srTimeOut;
end;
VReply := FMsgReply;
FWaitingForAnswer := False;
FReplyResponse := srError;
end;
finally
FLock.Leave;
end;
end;
function TIdHL7.GetMessage(var VMsg: String): TObject;
begin
assert(Assigned(self));
assert(Assigned(FLock));
assert(Assigned(FMsgQueue));
FLock.Enter;
try
if FMsgQueue.Count = 0 then
begin
Result := NIL;
end
else
begin
Result := FMsgQueue[0];
TQueuedMessage(Result)._AddRef;
VMsg := TQueuedMessage(Result).FMsg;
FMsgQueue.Delete(0);
FHndMsgQueue.Add(Result);
end;
finally
FLock.Leave;
end;
end;
procedure TIdHL7.SendReply(AMsgHnd: TObject; AReply: String);
var
qm: TQueuedMessage;
begin
assert(Assigned(self));
assert(Assigned(AMsgHnd));
assert(AReply <> '', 'Attempt to send an empty reply'); {do not localize}
assert(Assigned(FLock));
FLock.Enter;
try
qm := AMsgHnd as TQueuedMessage;
qm.FReply := AReply;
qm._Release;
FHndMsgQueue.Delete(FHndMsgQueue.IndexOf(AMsgHnd));
finally
FLock.Leave;
end;
qm.FEvent.SetEvent;
end;
end.