From d6ad951e55d139a7b0d15ac4923a24c9cfcd6b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=97=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D1=86=D0=BA=D0=B8=D0=B9?= Date: Wed, 18 Oct 2023 22:41:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- baseconnection.pas | 422 ++++++++++++++++++ cgi_daemon.lfm | 8 + cgi_daemon.pas | 67 +++ cgi_mapper.lfm | 20 + cgi_mapper.pas | 34 ++ cgicommand.pas | 13 + cgidm.lfm | 38 ++ cgidm.pas | 363 ++++++++++++++++ cgireport.pas | 329 ++++++++++++++ connectionsdmunit.lfm | 32 ++ connectionsdmunit.pas | 538 +++++++++++++++++++++++ exttypes.pas | 422 ++++++++++++++++++ fr_utils.pas | 91 ++++ lms_cgi.lpi | 108 +++++ lms_cgi.lpr | 166 +++++++ lmsreport.ico | Bin 0 -> 133345 bytes lmsreport.ini | 7 + lmsreport.lpi | 161 +++++++ lmsreport.lpr | 38 ++ maintcpserver.lfm | 146 +++++++ maintcpserver.pas | 182 ++++++++ numberinwords.pas | 464 ++++++++++++++++++++ reportdmunit.lfm | 94 ++++ reportdmunit.pas | 693 ++++++++++++++++++++++++++++++ tcpclient.pas | 111 +++++ tcpserver.pas | 134 ++++++ tcpthreadhelper.pas | 1047 +++++++++++++++++++++++++++++++++++++++++++++ xpReportUtil.pas | 127 ++++++ xpaccessunit.pas | 43 ++ xpmemparammanagerunit.pas | 141 ++++++ xputilunit.pas | 69 +++ 32 files changed, 6109 insertions(+), 1 deletion(-) create mode 100644 baseconnection.pas create mode 100644 cgi_daemon.lfm create mode 100644 cgi_daemon.pas create mode 100644 cgi_mapper.lfm create mode 100644 cgi_mapper.pas create mode 100644 cgicommand.pas create mode 100644 cgidm.lfm create mode 100644 cgidm.pas create mode 100644 cgireport.pas create mode 100644 connectionsdmunit.lfm create mode 100644 connectionsdmunit.pas create mode 100644 exttypes.pas create mode 100644 fr_utils.pas create mode 100644 lms_cgi.lpi create mode 100644 lms_cgi.lpr create mode 100644 lmsreport.ico create mode 100644 lmsreport.ini create mode 100644 lmsreport.lpi create mode 100644 lmsreport.lpr create mode 100644 maintcpserver.lfm create mode 100644 maintcpserver.pas create mode 100644 numberinwords.pas create mode 100644 reportdmunit.lfm create mode 100644 reportdmunit.pas create mode 100644 tcpclient.pas create mode 100644 tcpserver.pas create mode 100644 tcpthreadhelper.pas create mode 100644 xpReportUtil.pas create mode 100644 xpaccessunit.pas create mode 100644 xpmemparammanagerunit.pas create mode 100644 xputilunit.pas diff --git a/.gitignore b/.gitignore index 61043be..a8e6c49 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ backup/ *.bak lib/ - +out/ # Application bundle for Mac OS *.app/ diff --git a/baseconnection.pas b/baseconnection.pas new file mode 100644 index 0000000..32d9798 --- /dev/null +++ b/baseconnection.pas @@ -0,0 +1,422 @@ +unit baseconnection; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils, extTypes, Contnrs, cgiDM,reportDMUnit; +type + { TBaseConnection } + TBaseConnection=class; + + { TCommand } + + TCommand=class + protected + fData, + fResult: TCommandData; + fCommandID: string; + fStatus: integer; + fcurrentStage: string; + fconnect: TBaseConnection; + TimeOut: single; + fisDone,fisFinished: boolean; + fIsError: boolean; + fSubClass: string; + public + AccessTime: TDateTime; + + property Arguments: TCommandData read fData; + property Results: TCommandData read fResult; + property CommandID: string read fCommandID; + property Status: integer read fStatus; + property isDone: boolean read fIsDone; + property Error: boolean read fIsError; + property isFinished: boolean read fIsFinished; + property CurrentStage: string read fCurrentStage; + property Connect: TBaseConnection read fConnect; + constructor Create(aConnect: TBaseConnection; ASubClass: string); + destructor Destroy; override; + procedure doRun; + procedure Done; + function Run: boolean; virtual; abstract; + class function CommandName: string; virtual; abstract; + class function CommandSubClass: string; virtual; abstract; + function CheckArgs(out Errors: TStrings): boolean; virtual; abstract; + function ParseCommand(ACode: DWORD; iParam: QWORD; ACommand: string; Args: TStrings; intArgs: TParamArray; CmdData: TStream; out Errors: TStrings): boolean; + function ParseArguments(Args: TStrings; out Errors: TStrings): boolean; virtual; abstract; + procedure Log(msg: string); + function ProcessOptionValues(ParamName: string; out Answer: string; out RetValue: QWORD; out OptionValues: TStrings): boolean; virtual; abstract; + end; + TCommandClass=class of TCommand; + + { TCommandCollection } + TCommandCollection=Class; + TCommandCollection=Class(TClassList) + private + class var fCollection: TCommandCollection; + public + class procedure Register(ACommand: TCommandClass); + class function Find(Action,SubClass: string): TCommandClass; + class procedure Init; + class procedure Done; + end; + + TBaseConnection=class(TThread) + private + fOwner:TObject; + fLogger: TLogger; + fConnectionID: string; + fTimeout: integer; + fProcessor: TNIDBDM; + fReportProcessor: TReportDM; + Commands: TStrings; + DoneCommands: TList; + fCreated, + fCommandReceived, + fCommandCompleted: TDateTime; + nCommandComplete: integer; + nCommandReceived: integer; + nCommandReady: integer; + nErrors: integer; + + procedure CleanDone; + public + Host: string; + port: integer; + DataBase: string; + User: string; + UserID: integer; + LastAccess: TDateTime; + procedure Log(sender: TObject; msg: string); + property Created: TDateTime read fCreated; + property LastReceive: TDateTime read fCommandReceived; + property LastComplete: TDateTime read fCommandCompleted; + property CountReceived: integer read nCommandReceived; + property CountCompleted: integer read nCommandComplete; + property CountReady: integer read nCommandReady; + property CountErrors: integer read nErrors; + property Owner: TObject read fOwner; + property ConnectionID: string read fConnectionID; + property Processor: TNIDBDM read fProcessor; + property ReportProcessor: TReportDM read fReportProcessor; + procedure Init; + constructor Create(AOwner: TObject;ATimeOut: integer; aLogger: TLogger); + destructor Destroy; override; + // CommandID,Param,ACommand,Fields,iParam.Data + function AddCommand(ACode: DWORD; iParam: QWORD; ACommandClass,ACommandName: string; Arguments: TStrings; intArgs: TParamArray; CmdData: TStream; out ID: string; out retCode:DWORD; out Errors: TStrings): boolean; + function RunCommand(ACommand: TCommand): boolean; + function FindCommand(IDCommand: string): TCommand; + procedure Idle; + procedure Execute; override; + function ProcessOptionValues(ReportName,ParamName: string; ParamValues: TStrings; out Answer: string; out RetValue: QWORD; out OptionValues: TStrings): boolean; + class function newID: string; + end; + +implementation +{ TBaseConnection } + +procedure TBaseConnection.Init; +begin + Processor.connection.RemoteHost:=Host; + Processor.connection.RemotePort:=Port; + Processor.connection.Database:=DataBase; + Processor.OpenConnection; +end; + + +constructor TBaseConnection.Create(AOwner: TObject; ATimeOut: integer; + aLogger: TLogger); +begin + inherited Create(true); + fTimeout:=ATimeOut; + fOwner := AOwner; + flogger := ALogger; + fProcessor:=TNIDBDM.Create(nil); + fProcessor.logger:=aLogger; + fReportProcessor:=TReportDM.Create(nil); + fReportProcessor.NidbData := fProcessor; + Commands:=TStringList.Create; + DoneCommands:=TList.Create; + fCreated := now(); + fCommandReceived := 0; + fCommandCompleted := 0; + nCommandComplete:=0; + nCommandReceived:=0; + nCommandReady:=0; + nErrors:=0; + fConnectionID:=newID; + +end; + +destructor TBaseConnection.Destroy; +begin + log(self,'Destroy'); + Processor.Free; + Commands.Free; + DoneCommands.Free; + inherited; +end; + +function TBaseConnection.AddCommand(ACode: DWORD; iParam: QWORD; ACommandClass, + ACommandName: string; Arguments: TStrings; intArgs: TParamArray; + CmdData: TStream; out ID: string; out retCode: DWORD; out Errors: TStrings + ): boolean; +var + cc: TCommandClass; + cmd: TCommand; +begin + log(self,'AddCommand '+ACommandClass+ ' '+ACommandName); + fCommandReceived:=Now(); + cc := TCommandCollection.Find(ACommandClass,ACommandName); + if assigned(cc) then + begin + cmd := cc.Create(self,ACommandName); + cmd.AccessTime:=NOW(); + result := cmd.ParseCommand(ACode,iParam,ACommandName,Arguments,intArgs,CmdData, Errors); + if result then + begin + Commands.AddObject(ACommandName,cmd); + ID := cmd.CommandID; + retCode := Commands.Count; + end + else + begin + ID := 'неверные параметры запроса'; + retCode := ErrorArguments; + inc(nErrors); + cmd.fIsError:=true; + cmd.Done; + DoneCommands.Add(cmd); + end; + inc(nCommandReceived); + + + + end + else + inc(nErrors); +end; + +function TBaseConnection.RunCommand(ACommand: TCommand): boolean; +begin + ACommand.doRun(); + log(Self,'complete '+ACommand.CommandID); + fCommandCompleted:=Now(); + inc(nCommandReady); +end; + +function TBaseConnection.FindCommand(IDCommand: string): TCommand; +var + i: integer; +begin + for i := 0 to Commands.Count-1 do + if (Commands.Objects[i] as TCommand).CommandID=IDCommand then + begin + result := Commands.Objects[i] as TCommand; + result.AccessTime:=now(); + exit; + end; + for i := DoneCommands.Count-1 downto 0 do + if TCommand(DoneCommands[i]).CommandID=IDCommand then + begin + result := TCommand(DoneCommands[i]); + result.AccessTime:=now(); + exit; + end; + result := nil; +end; + +procedure TBaseConnection.Idle; +var + d: TDateTime; +begin + d := Created; + if LastAccess>d then d := LastAccess; + if (now()-d)*24*60>fTimeout then + begin + log(self,'TIMEOUT'); + terminate; + end + else + fProcessor.ExecuteSQL('SELECT 1'); +end; + +procedure TBaseConnection.CleanDone; +var + i: integer; + cmd: TCommand; +begin + for i := DoneCommands.Count-1 downto 0 do + begin + cmd := TCommand(DoneCommands[i]); + if cmd.isDone or (now()-cmd.AccessTime > cmd.TimeOut) then + begin + cmd.free; + DoneCommands.Delete(i); + Dec(nCommandReady); + + end; + end; +end; + +procedure TBaseConnection.Log(sender: TObject; msg: string); +begin + if assigned(fLogger) then + flogger(sender,msg); +end; + +procedure TBaseConnection.Execute; +var + cmd: TCommand; +begin + log(self,'started'); + while not terminated do + begin + while Commands.Count>0 do + begin + cmd := Commands.Objects[0] as TCommand; + log(self,format('run(%s) %s %s ',[cmd.CommandID,cmd.CommandName, cmd.CommandSubClass])); + try + RunCommand(cmd); + finally + Commands.Delete(0); + DoneCommands.Add(cmd); + end; + end; + CleanDone; + sleep(100); + end; + log(self,'finished'); +end; + +function TBaseConnection.ProcessOptionValues(ReportName, ParamName: string; + ParamValues: TStrings; out Answer: string; out RetValue: QWORD; out + OptionValues: TStrings): boolean; +var + c: TCommand; + tmp: TStrings; +begin + with TCommandCollection.Find('report',Reportname).Create(self,ReportName) do + try + ParseCommand(0,0,ReportName,ParamValues,[],nil,tmp); + if assigned(tmp) then FreeAndNil(tmp); + result := ProcessOptionValues(ParamName,answer,RetValue,OptionValues); + finally + free + end; +end; + +class function TBaseConnection.newID: string; +var + g: TGUID; + i: integer; +begin + createguid(g); + result := inttohex(g.D1,8)+inttohex(g.D2,4)+inttohex(g.D3,4); + for i := 0 to 7 do + result := result + inttohex(g.D4[i],2); +end; + + +{ TCommandCollection } + +class procedure TCommandCollection.Register(ACommand: TCommandClass); +begin + fCollection.Add(ACommand); +end; + +class function TCommandCollection.Find(Action, SubClass: string): TCommandClass; +var + i: integer; +begin + for i := 0 to fCollection.Count-1 do + if fCollection.Items[i].InheritsFrom(TCommand) and SameText(Action, TCommandClass(fCollection.Items[i]).CommandName) and SameText(SubClass, TCommandClass(fCollection.Items[i]).CommandSubClass) then + begin + result := TCommandClass(fCollection.Items[i]) ; + exit; + end; + for i := 0 to fCollection.Count-1 do + if fCollection.Items[i].InheritsFrom(TCommand) and SameText(Action, TCommandClass(fCollection.Items[i]).CommandName) and SameText('', TCommandClass(fCollection.Items[i]).CommandSubClass) then + begin + result := TCommandClass(fCollection.Items[i]) ; + exit; + end; + result := nil; +end; + +class procedure TCommandCollection.Init; +begin + fCollection := TCommandCollection.Create; +end; + +class procedure TCommandCollection.Done; +begin + fCollection.Free; +end; + +{ TCommand } + +constructor TCommand.Create(aConnect: TBaseConnection; ASubClass: string); +begin + fconnect := AConnect; + fSubClass := ASubClass; + fStatus:=StatusWaiting; + fcurrentStage := 'в очереди'; + TimeOut:=1/24/4; + fCommandID:=TBaseConnection.newID; +end; + +destructor TCommand.Destroy; +begin + if assigned(fData) then fData.Free; + if assigned(fResult) then fResult.free; + inherited Destroy; +end; + +procedure TCommand.doRun; +begin + fStatus:=StatusProcessing; + fcurrentStage := 'исполняется'; + try + if Run then + begin + fStatus:=StatusComplete; + fcurrentStage := 'завершена'; + end + else + begin + fStatus := StatusError; + fcurrentStage := 'завершена c ошибкой'; + end; + + except on e: Exception do + begin + fStatus:=StatusError; + fcurrentStage := 'error'; + Results.Name:=e.Message; + end; + end; +end; + +procedure TCommand.Done; +begin + fisDone:=true; +end; + +function TCommand.ParseCommand(ACode: DWORD; iParam: QWORD; ACommand: string; + Args: TStrings; intArgs: TParamArray; CmdData: TStream; out Errors: TStrings + ): boolean; +begin + self.fData := TCommandData.Create(ACode,iParam,ACommand,Args,intArgs,cmdData); + result := ParseArguments(fData.Keys,Errors); +end; + +procedure TCommand.Log(msg: string); +begin + connect.log(self, self.CommandID+#09+msg) +end; + +end. + diff --git a/cgi_daemon.lfm b/cgi_daemon.lfm new file mode 100644 index 0000000..48f798d --- /dev/null +++ b/cgi_daemon.lfm @@ -0,0 +1,8 @@ +object LMSReportCGI: TLMSReportCGI + OldCreateOrder = False + OnStart = DataModuleStart + Height = 150 + HorizontalOffset = 840 + VerticalOffset = 384 + Width = 445 +end diff --git a/cgi_daemon.pas b/cgi_daemon.pas new file mode 100644 index 0000000..9b9d2b5 --- /dev/null +++ b/cgi_daemon.pas @@ -0,0 +1,67 @@ +unit cgi_daemon; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, DaemonApp; + +type + TLMSReportCGI=class; + { TDaemonThread } + + TDaemonThread=class(TThread) + fOwner: TLMSReportCGI; + procedure Execute;override; + constructor Create(AOwner: TLMSReportCGI); + end; + + { TLMSReportCGI } + + TLMSReportCGI = class(TDaemon) + procedure DataModuleStart(Sender: TCustomDaemon; var OK: Boolean); + private + workThread: TDaemonThread; + public + + end; + +var + LMSReportCGI: TLMSReportCGI; + +implementation + +procedure RegisterDaemon; +begin + RegisterDaemonClass(TLMSReportCGI) +end; + +{$R *.lfm} + +{ TLMSReportCGI } + +procedure TLMSReportCGI.DataModuleStart(Sender: TCustomDaemon; var OK: Boolean); +begin + workThread := TDaemonThread(self); + workThread.Start; +end; + +{ TDaemonThread } + +procedure TDaemonThread.Execute; +begin + +end; + +constructor TDaemonThread.Create(AOwner: TLMSReportCGI); +begin + inherited Create(true); + fOwner:=AOwner; +end; + + +initialization + RegisterDaemon; +end. + diff --git a/cgi_mapper.lfm b/cgi_mapper.lfm new file mode 100644 index 0000000..4a1f5eb --- /dev/null +++ b/cgi_mapper.lfm @@ -0,0 +1,20 @@ +object DaemonMapper1: TDaemonMapper1 + DaemonDefs = < + item + DaemonClassName = 'TLMSReportCGI' + Name = 'lmsRepCGI' + Description = 'Служба создания отчетовв LMS 2' + DisplayName = 'LMS2 (отчеты)' + Options = [doAllowStop, doAllowPause] + WinBindings.Dependencies = <> + WinBindings.StartType = stBoot + WinBindings.WaitHint = 0 + WinBindings.IDTag = 0 + WinBindings.ServiceType = stWin32 + WinBindings.ErrorSeverity = esIgnore + WinBindings.AcceptedCodes = [] + LogStatusReport = False + end> + Left = 1065 + Top = 332 +end diff --git a/cgi_mapper.pas b/cgi_mapper.pas new file mode 100644 index 0000000..03bff21 --- /dev/null +++ b/cgi_mapper.pas @@ -0,0 +1,34 @@ +unit cgi_mapper; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, DaemonApp; + +type + TDaemonMapper1 = class(TDaemonMapper) + private + + public + + end; + +var + DaemonMapper1: TDaemonMapper1; + +implementation + +procedure RegisterMapper; +begin + RegisterDaemonMapper(TDaemonMapper1) +end; + +{$R *.lfm} + + +initialization + RegisterMapper; +end. + diff --git a/cgicommand.pas b/cgicommand.pas new file mode 100644 index 0000000..b08bb43 --- /dev/null +++ b/cgicommand.pas @@ -0,0 +1,13 @@ +unit cgiCommand; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils; + +implementation + +end. + diff --git a/cgidm.lfm b/cgidm.lfm new file mode 100644 index 0000000..adfe6a6 --- /dev/null +++ b/cgidm.lfm @@ -0,0 +1,38 @@ +object NIDBDM: TNIDBDM + OnCreate = DataModuleCreate + OnDestroy = DataModuleDestroy + OldCreateOrder = False + Height = 150 + HorizontalOffset = 417 + VerticalOffset = 131 + Width = 150 + object nnzQuery1: TnnzQuery + FieldDefs = <> + ReadOnly = True + FilterOptions = [] + Left = 25 + Top = 36 + end + object frxReport1: TfrxReport + Version = '2023.1' + DotMatrixReport = False + IniFile = '\Software\Fast Reports' + PreviewOptions.Buttons = [pbPrint, pbLoad, pbSave, pbExport, pbZoom, pbFind, pbOutline, pbPageSetup, pbTools, pbEdit, pbNavigator, pbExportQuick, pbCopy, pbSelection] + PreviewOptions.Zoom = 1 + PrintOptions.Printer = 'Default' + PrintOptions.PrintOnSheet = 0 + ReportOptions.CreateDate = 45099.7272582292 + ReportOptions.LastChange = 45099.7272582292 + ScriptLanguage = 'PascalScript' + ScriptText.Strings = ( + 'begin' + '' + 'end.' + ) + Left = 64 + Top = 92 + Datasets = <> + Variables = <> + Style = <> + end +end diff --git a/cgidm.pas b/cgidm.pas new file mode 100644 index 0000000..2869fec --- /dev/null +++ b/cgidm.pas @@ -0,0 +1,363 @@ +unit cgiDM; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils, nnz_data_components, frxClass, nnzDBClient,DB,extTypes; + +type + + { TNIDBDM } + + TNIDBDM = class(TDataModule) + frxReport1: TfrxReport; + nnzQuery1: TnnzQuery; + procedure DataModuleCreate(Sender: TObject); + procedure DataModuleDestroy(Sender: TObject); + private + fcon: TnnzConnection; + fMid: integer; + fLogin: string; + fID: TGUID; + fLastConnectTime: TDateTime; + flogger: TLogger; + class function DeleteLastChar(const St: String; const CharCount: integer=1): String; + public + class function FloatAsSQL(Value: Double): String; + class function PeriodAsSQL(BegDate, EndDate: TDateTime): string; + class function DateAsSQL(Date: TDateTime): String; + class function DateTimeAsSQL(Stamp: TDateTime): String; + class function CurrencyAsSQL(Value: Currency): string; + class function VariantAsSQL(const Value: Variant): String; + class function BooleanAsSQL(Value: Boolean): String; + class function StringAsSQL(const St: String): String; + class function StringAsJSON(const St: String): String; + class function StreamAsSQL(s: TStream): String; + class function BytesAsSQL(const data: array of byte; dataSize: integer): string; + class function IntArrayAsSQL(const data: array of integer): string; + class procedure UpdateWithArguments(var code: string; const Arguments: TStrings); + property logger: TLogger read flogger write flogger; + property connection: TnnzConnection read fCon; + function QueryValue(ASQL: string; Default: string=''): string; + function QueryIntValue(ASQL: string): integer; + function GetData(ASQL: string): TDataSet; + function CheckUser(const login,password: string; out UserID: integer): boolean; + procedure OpenConnection; + procedure log(Sender: TObject; msg: string); + procedure LogError(Sender: TObject; e: Exception; msg: string); + procedure ExecuteSQL(ASQL: string); + end; + +var + NIDBDM: TNIDBDM; + +implementation +uses + variants,LazUTF8; +{$R *.lfm} + +{ TNIDBDM } + +procedure TNIDBDM.DataModuleCreate(Sender: TObject); +begin + log(self,'connecting'); + fcon := TnnzConnection.Create(self); + + +end; + +procedure TNIDBDM.DataModuleDestroy(Sender: TObject); +begin + log(sender,'destroy'); + fcon.Connected:=false; + fcon.free; +end; + +class function TNIDBDM.DeleteLastChar(const St: String; + const CharCount: integer): String; +begin + Result := utf8Copy(St, 1, utf8Length(St) - CharCount); +end; + +class function TNIDBDM.FloatAsSQL(Value: Double): String; +var + fs: TFormatSettings; +begin + fs.DecimalSeparator := '.'; + Result := FloatToStr(Value, fs); +end; + + +class function TNIDBDM.PeriodAsSQL(BegDate, EndDate: TDateTime): string; +begin + if (BegDate <= 2) and (EndDate <= 2) then + Result := '>=''1900-01-01''' + else + Result := 'BETWEEN ' + DateAsSQL(BegDate) + ' AND ' + DateAsSQL(EndDate); +end; + + +class function TNIDBDM.DateAsSQL(Date: TDateTime): String; +begin + if FormatSettings.ShortMonthNames[1] <> 'JAN' then + begin + FormatSettings.ShortMonthNames[1] := 'JAN'; + // без этого дурит MSSQL7/MSSQL2000, который не понимает + FormatSettings.ShortMonthNames[2] := 'FEB'; // дату в формате '13 окт 1999' + FormatSettings.ShortMonthNames[3] := 'MAR'; + FormatSettings.ShortMonthNames[4] := 'APR'; + FormatSettings.ShortMonthNames[5] := 'MAY'; + FormatSettings.ShortMonthNames[6] := 'JUN'; + FormatSettings.ShortMonthNames[7] := 'JUL'; + FormatSettings.ShortMonthNames[8] := 'AUG'; + FormatSettings.ShortMonthNames[9] := 'SEP'; + FormatSettings.ShortMonthNames[10] := 'OCT'; + FormatSettings.ShortMonthNames[11] := 'NOV'; + FormatSettings.ShortMonthNames[12] := 'DEC'; + end; + if Date < 2 then + Date := 2; // приводим к 1 января 1900 - это ноль для SMALLDATETIME + Result := QuotedStr(FormatDateTime('yyyy-mm-dd', Date)); +end; +class function TNIDBDM.DateTimeAsSQL(Stamp: TDateTime): String; +begin + // Result := FloatToStr(Real(Stamp) - 2); старый вариант - ldm + Result := ''; + DateTimeToString(Result, 'hh:nn:ss', Stamp); + Result := DeleteLastChar(DateAsSQL(Stamp)) + ' ' + Result + ''''; +end; + +class function TNIDBDM.CurrencyAsSQL(Value: Currency): string; +begin + Result := StringAsSQL(CurrToStr(Value)); +end; + +class function TNIDBDM.VariantAsSQL(const Value: Variant): String; +var + Stream: TStringStream; +begin + case VarType(Value) of + varNull: + Result := 'NULL'; + varEmpty: + Result := 'NULL'; + varSmallInt, varInteger, varShortInt, varByte, varWord, varLongWord, + varInt64, varUInt64: + Result := VarToStr(Value); + varSingle, varCurrency: + Result := CurrencyAsSQL(Value); + varDouble: + Result := FloatAsSQL(Value); + varDate: + Result := DateAsSQL(Value); + varOleStr: + begin + Stream := TStringStream.Create(Value); + try + Result := StreamAsSQL(Stream); + finally + Stream.Free; + end; + end; + varBoolean: + Result := BooleanAsSQL(Value); + varString, varUString: + Result := StringAsSQL(Value); + else + raise Exception.Create('VariantAsSQL: Неверный тип ' + VarToStr(Value)); + end; +end; + +class function TNIDBDM.BooleanAsSQL(Value: Boolean): String; +begin + if value then result := 'true' else result := 'false'; +end; + + +class function TNIDBDM.StreamAsSQL(s: TStream): String; +var + i: Integer; + b: Byte; +begin + result := 'null'; + if s.size=0 then exit; + //Result := '0x'; //MySQL + Result := 'E''\\x'; //PostgreSQL + s.Position := 0; + for i := 1 to s.Size do + begin + s.Read(b, 1); + Result := Result + IntToHex(b, 2); + end; + result := result +''''; +end; + +class function TNIDBDM.BytesAsSQL(const data: array of byte; + dataSize: integer): string; +var + i: integer; + b: byte; +begin + result := 'null'; + if datasize=0 then exit; + Result := 'E''\\x'; //PostgreSQL + for i := 0 to dataSize-1 do + begin + b := data[i]; + Result := Result + IntToHex(b, 2); + end; + result := result +''''; +end; + + +class function TNIDBDM.IntArrayAsSQL(const data: array of integer): string; +var + i: integer; +begin + if length(data)=0 then + result := 'ARRAY[]::integer[]' + else + begin + result := inttostr(data[low(data)]); + for i := low(data)+1 to high(data) do + result := result +','+inttostr(data[i]); + result := format('ARRAY[%s]::integer[]',[result]); + end; +end; + +class procedure TNIDBDM.UpdateWithArguments(var code: string; + const Arguments: TStrings); +var + i: integer; + s,v: string; +begin + for i := 0 to Arguments.Count-1 do + begin + s := Arguments.Names[i]; + v := Arguments.Values[s]; + Code := StringReplace(Code,'{s#'+s+'}', TNidbDM.StringAsSQL(v),[rfReplaceAll]); + Code := StringReplace(Code,'{d#'+s+'}',TNidbDM.StringAsSQL(v),[rfReplaceAll]); + Code := StringReplace(Code,'{#'+s+'}',v,[rfReplaceAll]); + end; +end; + + + + +class function TNIDBDM.StringAsSQL(const St: String): String; +var + len,i: Cardinal; + c: Char; + hasSpecial: boolean; +begin +// Result := QuotedStr(StringReplace(St, '\', '\\', +// [rfReplaceAll, rfIgnoreCase])); + Result:=''; + len:=Length(St); + hasSpecial := false; + for i:=1 to len do begin + c:=St[i]; + hasSpecial := hasSpecial or (c in [#0,'''','"','\',#8,#10,#13,#9,#26]); + case(c) of + #0: Result:=Result+'\0'; + '''','"','\': Result:=Result+'\'+c; + #8: Result:=Result+'\b'; + #10: Result:=Result+'\n'; + #13: Result:=Result+'\r'; + #9: Result:=Result+'\t'; + #26: Result:=Result+'\Z'; + else + Result:=Result+c; + end; + end; + Result:=''''+Result+''''; + if hasSpecial then Result :='E'+Result; + +end; + +class function TNIDBDM.StringAsJSON(const St: String): String; +begin + result := st; + result := UTF8StringReplace(result,'\','\\',[rfReplaceAll]); + result := UTF8StringReplace(result,'"','\"',[rfReplaceAll]); +end; + + +function TNIDBDM.QueryValue(ASQL: string; Default: string): string; +begin + log(self,'QueryValue'#13#10+ASQL); + with TnnzQuery.Create(self) do + try + Connection := fcon; + SQL.Text:=ASQL; + Open; + if not eof and not Fields[0].IsNull then result := Fields[0].AsString else result := Default; + finally + free; + end; +end; + +function TNIDBDM.QueryIntValue(ASQL: string): integer; +begin + log(self,'QueryIntValue'#13#10+ASQL); + with TnnzQuery.Create(self) do + try + Connection := fcon; + SQL.Text:=ASQL; + Open; + if not eof then result := Fields[0].AsInteger else result := 0; + finally + free; + end; +end; + + +function TNIDBDM.GetData(ASQL: string): TDataSet; +begin + log(self,'getData '#13#10+ASQL); + result := TnnzQuery.Create(self); + with result as TnnzQuery do + begin + Connection := fcon; + SQL.Text:=ASQL; + Open; + + end; +end; + +function TNIDBDM.CheckUser(const login, password: string; out UserID: integer + ): boolean; +begin + log(self,'CheckUser'); + UserID := QueryIntValue(format('Select coalesce((select min(p.mid) from people p where login=%s and password=%s),0) ',[StringAsSQL(login),StringAsSQl(password)])); + result := UserID>0; +end; + +procedure TNIDBDM.OpenConnection; +begin + log(self,'OpenConnection'); + fcon.Connected:=true; + fcon.Identify; +end; + +procedure TNIDBDM.log(Sender: TObject; msg: string); +begin + if assigned(flogger) then + flogger(Sender,msg); +end; + +procedure TNIDBDM.LogError(Sender: TObject; e: Exception; msg: string); +begin + log(Sender,'!!ERROT at '+msg+#13#10+e.ClassName+#13#10+e.message); +end; + +procedure TNIDBDM.ExecuteSQL(ASQL: string); +begin + log(self,'ExecuteSQL '+ASQL); + connection.ExecuteSQL(ASQL); +end; + +end. + diff --git a/cgireport.pas b/cgireport.pas new file mode 100644 index 0000000..ac99e17 --- /dev/null +++ b/cgireport.pas @@ -0,0 +1,329 @@ +unit cgiReport; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils, baseconnection; +type + { TReportCommand } + + TReportCommand=class(TCommand) + private + procedure CreateVariablesTable; + procedure UpdateCodeWithArguments(var code: string); + procedure SetStage(Sender:TObject; stageName: string); + public + ReportID: integer; + ReportName: string; + ReportTitle: string; + ReportCode: string; + Varcode: string; + class function CommandName: string; override; + class function CommandSubClass: string; override; + procedure Prepare; virtual; + procedure PrepareVars; virtual; + procedure FillVars; + function Run: boolean; override; + function ParseArguments(Args: TStrings; out Errors: TStrings): boolean; override; + function ProcessOptionValues(ParamName: string; out Answer: string; out RetValue: QWORD; out OptionValues: TStrings): boolean; override; + end; + +implementation +uses + cgiDM,extTypes,reportDMUnit, types, strutils, LazUTF8; +{ TReportCommand } + +procedure TReportCommand.CreateVariablesTable; +begin + connect.Processor.ExecuteSQL( + 'drop table if exists tmp_report_variables; '+ + 'create temporary table tmp_report_variables ( '+ + 'name character varying,'+ + 'value_string character varying, '+ + 'value_int integer, '+ + 'var_type integer '+ + '); ' + ); +end; + +procedure TReportCommand.UpdateCodeWithArguments(var code: string); +begin + TNIDBDM.UpdateWithArguments(code,Arguments.Keys); + Code := StringReplace(Code,'{#user}',inttostr(self.Connect.UserID),[rfReplaceAll]); +end; + +procedure TReportCommand.SetStage(Sender: TObject; stageName: string); +begin + fcurrentStage:=format('выполняется (%s)',[stageName]); +end; + +class function TReportCommand.CommandName: string; +begin + result := 'report'; +end; + +class function TReportCommand.CommandSubClass: string; +begin + result := ''; +end; + +procedure TReportCommand.Prepare; +var + ASQL: string; + v: string; + d: TStringDynArray; + i: integer; +begin + ReportCode := connect.Processor.QueryValue(format('select report_routine from xp_report_cgi where cgi_name=%s',[TNidbDM.StringAsSQL(ReportName)])); + UpdateCodeWithArguments(ReportCode); + if reportcode<>'' then + connect.Processor.ExecuteSQL(format('select %s;',[ReportCode])); + ASQL := format( 'select array_to_string(extra_routines,'';'') as v from xp_report_cgi where cgi_name=%s',[TNidbDM.StringAsSQL(ReportName)]); + v := connect.Processor.QueryValue(ASQL); + if v>'' then + begin + d := SplitString(v,';'); + for i := low(d) to high(d) do + begin + v := d[i]; + UpdateCodeWithArguments(v); + if v<>'' then + connect.Processor.ExecuteSQL(v); + end; + end; +end; + +procedure TReportCommand.PrepareVars; +begin + VarCode := connect.Processor.QueryValue(format('select report_routine_vars from xp_report_cgi where cgi_name=%s',[TNidbDM.StringAsSQL(ReportName)])); + UpdateCodeWithArguments(VarCode); + if VarCode<>'' then + connect.Processor.ExecuteSQL(format('select %s;',[VarCode])); +end; + +procedure TReportCommand.FillVars; +var + ASQL: string; + q: string; + script: string; + vs: string; + vi: integer; +begin + log('FillVars'); + script := format('insert into tmp_report_variables(name,value_string,var_type) values(''UserName'',%s,0); ',[TNIDBDM.StringAsSQL(self.Connect.User)]); + ASQL := format('select name,query from xp_report_variables where xp_rpt_id=%d and var_type=0',[ReportID]); + with connect.Processor.GetData(ASQL) do + try + while not eof do + begin + log(FieldByName('name').asString); + q := FieldByName('query').AsString; + UpdateCodeWithArguments(q); + try + vs := connect.Processor.QueryValue(q); + + except + vs := ''; + end; + script := script + format(#13#10'insert into tmp_report_variables(name,value_string,var_type) values (%s,%s,0); ', + [TNIDBDM.StringAsSQL(fieldByName('name').asString),TNidbDM.StringAsSQL(vs)]); + Next; + end; + finally + free; + end; + ASQL := format('select name,query from xp_report_variables where xp_rpt_id=%d and var_type=1',[ReportID]); + with connect.Processor.GetData(ASQL) do + try + while not eof do + begin + log(FieldByName('name').asString); + q := FieldByName('query').AsString; + UpdateCodeWithArguments(q); + try + vi := connect.Processor.QueryIntValue(q); + + except + vi := 0; + end; + script := script + format('insert into tmp_report_variables(name,value_int,var_type) values (%s,%d,1); '#13#10, + [TNIDBDM.StringAsSQL(fieldByName('name').asString),vi]); + Next; + end; + finally + free; + end; + if script<>'' then + connect.Processor.ExecuteSQL(script); +end; + +function TReportCommand.Run: boolean; +var + i: integer; + s: string; + fileData: TStream; +begin + result := false; + fcurrentStage := 'исполняется (инициализация)'; + fileData := TMemoryStream.Create; + try + ReportID := connect.Processor.QueryIntValue(format('select xp_rpt_id from xp_report_cgi where cgi_name=%s',[TNidbDM.StringAsSQL(ReportName)])); + if ReportID<=0 then + begin + fResult := TCommandData.Create(ErrorArguments,0,'Отчет не найден',nil,[],nil); + exit; + end; + ReportTitle := connect.Processor.QueryValue(format('select r.name from xp_report_cgi g join xp_report r on r.xp_rpt_id=g.xp_rpt_id where cgi_name=%s',[TNidbDM.StringAsSQL(ReportName)])); + CreateVariablesTable; + log(ReportTitle); + connect.ReportProcessor.RecordID:=ReportID; + fcurrentStage := 'исполняется (подготовка)'; + try + Prepare; + except on e: Exception do + begin + connect.Processor.LogError(self,e,'prepare'); + fResult := TCommandData.Create(ErrorInternal,0,'Ошибка составления',nil,[],nil); + exit; + end; + end; + fcurrentStage := 'исполняется (настройка)'; + try + FillVars; + PrepareVars; + except on e: Exception do + begin + connect.Processor.LogError(self,e,'vars'); + fResult := TCommandData.Create(ErrorInternal,0,'Ошибка составления',nil,[],nil); + exit; + end; + end; + fcurrentStage := 'исполняется ()'; + try + connect.ReportProcessor.ExportReport(ftPDF,fileData,@SetStage); + except on e: Exception do + begin + connect.Processor.LogError(self,e,'ExportReport'); + fResult := TCommandData.Create(ErrorInternal,0,'Ошибка составления',nil,[],nil); + exit; + end; + end; + fResult := TCommandData.Create(0,fileData.size,ReportTitle+'.pdf',nil,[],fileData); + fileData.Seek(0,soFromBeginning); + result := true; + finally + fileData.Free; + end; +end; + +function TReportCommand.ParseArguments(Args: TStrings; out Errors: TStrings + ): boolean; +var + asql: string; + ids: string; + i: integer; +begin + result := false; + ids := ''; + for i := 0 to Arguments.Keys.Count-1 do + if ids='' then + ids := TNIDBDM.StringAsSQL(Arguments.Keys.Names[i]) + else + ids := ids + ','+ TNIDBDM.StringAsSQL(Arguments.Keys.Names[i]); + ReportName := Arguments.Keys.Values['name']; + + asql := format( + 'select p.name from xp_report_cgi c '+ + 'join xp_report_params p on p.xp_rpt_id=c.xp_rpt_id and coalesce(p.required,true) '+ + 'where c.cgi_name=%s and p.name not in (%s) '+ + 'order by fill_order,p.name ', + [TNIDBDM.StringAsSQL(ReportName),TNIDBDM.StringAsSQL(ids)]); + with Connect.Processor.GetData(asql) do + try + if not eof then + begin + Errors := TStringList.Create; + while not eof do + begin + Errors.add(format('"%s"',[TNIDBDM.StringAsJSON(FieldByName('name').AsString)])); + next; + end; + end + else + result := true; + finally + free; + end; + +end; + +function TReportCommand.ProcessOptionValues(ParamName: string; out + Answer: string; out RetValue: QWORD; out OptionValues: TStrings): boolean; +var + ASQL: string; + code,s,k,v: string; + i,p: integer; + d: TStringDynArray; +begin + result := false; + ASQL := format( + 'select source from xp_report_params p '+ + ' join xp_report_cgi c on c.xp_rpt_id=p.xp_rpt_id '+ + 'where c.cgi_name=%s and p.name=%s and p.type in (0,1,2,17) ', + [TNIDBDM.StringAsSQL(fSubClass), TNIDBDM.StringAsSQL(ParamName)]); + code := connect.Processor.QueryValue(ASQL); + if code='' then exit; + if code[1]='(' then + begin + OptionValues := TStringList.Create; + code := copy(code,2,length(code)-2); + d := splitstring(code,','); + for i := low(d) to high(d) do + begin + s:= d[i]; + p:=pos(':',s); + if p>1 then + begin + k := copy(s,1,p-1); + v := copy(s,p+1,length(s)); + OptionValues.add(format('{"id":"%s","value":"%s"}',[TNIDBDM.StringAsJSON(k),TNIDBDM.StringAsJSON(v)])); + end + else + OptionValues.add(format('"%s"',[TNIDBDM.StringAsJSON(s)])); + end; + end + else + begin + UpdateCodeWithArguments(code); + if pos('{',code)>0 then + begin + result := false; + Answer := 'недостаточно данных'; + exit; + end; + ASQL := code; + OptionValues := TStringList.Create; + if ASQL<>'' then + with connect.Processor.GetData(ASQL) do + try + while not eof do + begin + OptionValues.Add(format('{"id":"%d","value":"%s"}', + [Fields[0].AsInteger, TNIDBDM.StringAsJSON(Fields[1].AsString)])); + next; + end; + finally + free; + end; + + end; + result := true; +end; + + +Initialization + TCommandCollection.Register(TReportCommand); +end. + diff --git a/connectionsdmunit.lfm b/connectionsdmunit.lfm new file mode 100644 index 0000000..682f05b --- /dev/null +++ b/connectionsdmunit.lfm @@ -0,0 +1,32 @@ +object ConnectionsDM: TConnectionsDM + OnCreate = DataModuleCreate + OnDestroy = DataModuleDestroy + OldCreateOrder = False + Height = 150 + HorizontalOffset = 677 + VerticalOffset = 301 + Width = 533 + object Process1: TProcess + Active = False + Options = [] + Priority = ppNormal + StartupOptions = [] + ShowWindow = swoNone + WindowColumns = 0 + WindowHeight = 0 + WindowLeft = 0 + WindowRows = 0 + WindowTop = 0 + WindowWidth = 0 + FillAttribute = 0 + Left = 63 + Top = 37 + end + object Hash: TDCP_sha1 + Id = 2 + Algorithm = 'SHA1' + HashSize = 160 + Left = 240 + Top = 60 + end +end diff --git a/connectionsdmunit.pas b/connectionsdmunit.pas new file mode 100644 index 0000000..7320f03 --- /dev/null +++ b/connectionsdmunit.pas @@ -0,0 +1,538 @@ +unit ConnectionsDmUnit; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, Contnrs, SysUtils, types, process, cgiDM, reportDMUnit, LNet, + lnetbase,tcpserver, tcpthreadhelper, DCPsha1, extTypes,syncobjs, baseconnection; + +type + + { TCommand } + + + + + + { TConnectionsDM } + + TConnectionsDM = class(TDataModule) + Hash: TDCP_sha1; + Process1: TProcess; + procedure DataModuleCreate(Sender: TObject); + procedure DataModuleDestroy(Sender: TObject); + private + fLogFolder: string; + MainCon: TNIDBDM; + conlist: TList; + Input: TServerMainThread; + fDataHost: string; + fDataPort: integer; + fDataBase: string; + fServicePort: integer; + + fTimeOut: integer; + LogLock: TCriticalSection; + fRunning: boolean; + function getConnection(ID: string): TBaseConnection; + function NewConnection: TBaseConnection; + procedure Remove(ID: string); + procedure ClearConnections; + procedure ClearTerminated; + procedure ConnectNew(aSocket: TLSocket); + function ProcessLogin(UserName,UserPassword: string; out UserID: integer):boolean; + function ProcessArguments(ReportName: string; out RetValue: QWORD;out ReportTitle: string; out rValues: TStrings): boolean; + function ProcessReports(out rValues: TStrings): boolean; + function ProcessOptionValues(ReportName,ParamName: string; ParamValues: TStrings; out Answer: string; out RetValue: QWORD; out OptionValues: TStrings): boolean; + procedure LoadConfig; + public + property DataHost: string read fDataHost; + property DataPort: integer read fDataPort; + property DataBase: string read fDataBase; + procedure Log(Sender: TObject; msg: string); + procedure Start; + procedure Idle(Sender: TObject); + property Running: boolean read fRunning; + function ProcessRequest(Sender: TMainThread; + const CommandID:DWORD; const Param:QWord; const ACommand: string; const Fields: TStrings; const iParams: TParamArray; const Data: TStream; + out Code: DWORD; out RetValue: QWord; out Answer: string; out rValues: TStrings; out iValues: TParamArray; out ByteData: TStream ): boolean; + + end; + +var + ConnectionsDM: TConnectionsDM; + +implementation +uses + xpUtilUnit, strutils, xpAccessUnit, inifiles; +{$R *.lfm} + + + + +{ TConnectionsDM } + +procedure TConnectionsDM.DataModuleCreate(Sender: TObject); +begin + MainCon := TNIDBDM.Create(nil); + MainCon.logger:=@log; + LogLock := TCriticalSection.Create; + conList := TList.Create; + LoadConfig; + fRunning := false; + input := TServerMainThread.Create(@log,fServicePort,@ProcessRequest); +end; + +procedure TConnectionsDM.DataModuleDestroy(Sender: TObject); +begin + log(Sender,'Destroy'); + ClearConnections; + + if fRunning then + begin + Input.Terminate; + Input.WaitFor; + end; + Input.Free; + MainCon.Free; + LogLock.Free; + conList.Free; + +end; + +function TConnectionsDM.getConnection(ID: string): TBaseConnection; +var + i: integer; +begin + for i := 0 to conList.Count-1 do + if TBaseConnection(conlist[i]).ConnectionID=ID then + begin + result := TBaseConnection(conlist[i]); + result.LastAccess := NOW(); + exit; + end; + result := nil; +end; + +function TConnectionsDM.NewConnection: TBaseConnection; +var + g: TGUID; + s: string; + i: integer; +begin + result := TBaseConnection.Create(self,fTimeOut,@Log); + conlist.add(result); + result.Host:=DataHost; + result.port:=DataPort; + result.DataBase:=DataBase; + log(self, 'New '+result.ConnectionID); + result.Init; + +end; + +procedure TConnectionsDM.Remove(ID: string); +var + i: integer; +begin + + for i := conList.Count-1 downto 0 do + if TBaseConnection(conlist[i]).ConnectionID=ID then + begin + log(self,'terminate '+ID); + TBaseConnection(conlist[i]).terminate; + exit; + end; +end; + + +procedure TConnectionsDM.ClearConnections; +var + i: integer; + con: TBaseConnection; +begin + log(self,'ClearConnections'); + for i := 0 to conList.Count-1 do + begin + con := TBaseConnection(conlist[i]); + con.terminate; + con.WaitFor; + con.Free; + end; + conList.Clear; +end; + +procedure TConnectionsDM.ClearTerminated; +var + i: integer; + con: TBaseConnection; +begin + for i := conlist.Count-1 downto 0 do + begin + con := TBaseConnection(conlist[i]); + if con.Finished then + begin + log(self,'Destroy terminated '+con.ConnectionID); + con.free; + conlist.delete(i); + end; + end; +end; + +procedure TConnectionsDM.ConnectNew(aSocket: TLSocket); +begin +// aSocket +end; + +function TConnectionsDM.ProcessRequest(Sender: TMainThread; + const CommandID: DWORD; const Param: QWord; const ACommand: string; + const Fields: TStrings; const iParams: TParamArray; const Data: TStream; out + Code: DWORD; out RetValue: QWord; out Answer: string; out rValues: TStrings; + out iValues: TParamArray; out ByteData: TStream): boolean; +var + UserID: integer; + con: TBaseConnection; + userName,conID,cmdID: string; + cmd: TCommand; +begin + log(Self,'Process Request '+ACommand); + ClearTerminated; + result := false; + RetValue := 0; + Code := 0; + rValues := nil; + ByteData := nil; + setLength(iValues,0); + if ACommand='stop' then + begin + ClearConnections; + Input.Terminate; + fRunning:=false; + result := true; + exit; + end; + if ACommand='version' then + begin + result := true; + Answer := extTypes.version; + exit; + end; + if ACommand='arguments' then + begin + result := ProcessArguments(Fields.Values['name'],RetValue,Answer,rValues); + if not result then + begin + Code := ErrorArguments; + end; + exit; + end; + if ACommand='reports' then + begin + result := ProcessReports(rValues); + exit; + end; + if ACommand='login' then + begin + UserName :=Fields.Values['user']; + if ProcessLogin(UserName,EncryptText(Fields.Values['password']),UserID) then + begin + con := NewConnection; + con.User:=UserName; + con.UserID := UserID; + Answer := con.ConnectionID; + con.Start; + result := true; + end + else + begin + Answer := 'Invalid password'; + code := ErrorLogin; + end; + exit; + end; + conID := fields.Values['connect']; + con := getConnection(conID); + + if not assigned(con) or (con.Finished) then + begin + Answer := 'invalid connectionID'; + code := ErrorConnect; + exit; + end; + if ACommand='test' then + begin + result := true; + answer := 'OK'; + exit; + end; + if ACommand='logout' then + begin + result := true; + Answer := 'OK'; + Remove(con.ConnectionID); + exit; + end; + if ACommand='connectStatus' then + begin + result := true; + SetLength(iValues,7); + iValues[0] := round(con.Created*24*60*60*100); + iValues[1] := round(con.LastReceive*24*60*60*100); + iValues[2] := round(con.LastComplete*24*60*60*100); + iValues[3] := con.CountReceived; + iValues[4] := con.CountCompleted; + iValues[5] := con.CountReady; + iValues[6] := con.CountErrors; + Answer := 'OK'; + exit; + end; + if (ACommand='option_values') then + begin + result := con.ProcessOptionValues(fields.Values['report'],fields.Values['name'],fields,Answer,RetValue,rValues); + exit; + end; + + if (ACommand='status') or (ACommand='result') then + begin + cmdID := fields.Values['operation']; + cmd := con.FindCommand(cmdID); + if not assigned(cmd) then + begin + Answer := 'command not found'; + Code := ErrorCommand; + exit; + end; + if ACommand='status' then + begin + Answer := cmd.currentStage; + code := cmd.Status; + if (code=StatusComplete) and assigned(cmd.Results.Data) then + RetValue:=cmd.Results.Data.Size + else + RetValue := 0; + result := true; + exit; + end; + if ACommand='result' then + begin + if cmd.Status=StatusComplete then + begin + cmd.Results.AssignTo(Code,RetValue,Answer,rValues,iValues,ByteData); + cmd.Done; + result := true; + end + else + begin + Code := ErrorComplete; + Answer:='command not complete'; + end; + exit; + end; + + end; + result := con.AddCommand(CommandID,Param,ACommand,Fields.Values['name'],Fields,iValues,Data,Answer,Code, rValues); + +end; + +function TConnectionsDM.ProcessLogin(UserName, UserPassword: string; out UserID: integer): boolean; +var + ASQL: string; +begin + Result := MainCon.CheckUser(UserName,UserPassword,UserID); +end; + +function TConnectionsDM.ProcessArguments(ReportName: string; out + RetValue: QWORD; out ReportTitle: string; out rValues: TStrings): boolean; +var + ASQL: string; +begin + result := false; + rValues := TStringList.Create; + ASQL := format( + 'select r.xp_rpt_id,r.name as reportname,p.name as paramname, '+ + 'case p.type '+ + ' when 0 then ''A'' '+ + ' when 1 then ''ID'' '+ + ' when 2 then ''N'' '+ + ' when 3 then ''F'' '+ + ' when 4 then ''D'' '+ + ' when 5 then ''T'' '+ + ' when 6 then ''B'' '+ + ' when 17 then ''IDS'' '+ + 'end as type, '+ + + 'case coalesce(p.required,false) or p.def_val is null '+ + 'when true then ''!'' '+ + 'else p.def_val '+ + 'end as def_val, '+ + 'string_agg(''"'' || p.argument || ''"'','';'') as arguments, '+ + 'p.description '+ + 'from xp_report_cgi c '+ + ' join xp_report r on r.xp_rpt_id=c.xp_rpt_id '+ + ' left join ( '+ + ' select xp_rpt_id, type,name, required,def_val,description,fill_order, unnest(coalesce(arguments,array[null])) as argument '+ + ' from xp_report_params '+ + ')p on p.xp_rpt_id=r.xp_rpt_id '+ + 'where c.cgi_name=%0:s '+ + 'group by r.xp_rpt_id,r.name, p.name,p.type,p.required, p.def_val,p.description, p.fill_order '+ + 'order by p.fill_order, p.name ', + [TNIDBDM.StringAsSQL(ReportName)]); + with MainCon.GetData(ASQL) do + try + while not eof do + begin + ReportTitle := fieldByName('reportname').AsString; + rValues.Add(format('{"name":"%s","type":"%s","default":"%s","arguments":[%s],"description":"%s"}', + [fieldbyname('paramname').asString, fieldbyname('type').asString,fieldbyname('def_val').asString,fieldbyname('arguments').asString, TNIDBDM.StringAsJSON(fieldbyname('description').asString)])); + result := true; + next; + end; + finally + free; + end; + +end; + +function TConnectionsDM.ProcessReports(out rValues: TStrings): boolean; +var + ASQL: string; +begin + rValues := TStringList.Create; + ASQL := + 'select c.cgi_name,r.name as rep_name '+ + 'from xp_report_cgi c '+ + ' join xp_report r on r.xp_rpt_id=c.xp_rpt_id '+ + 'order by 2 '; + with MainCon.GetData(ASQL) do + try + while not eof do + begin + rValues.Add(format('{"name":"%s","title":"%s"}',[fieldbyname('cgi_name').asString, TNIDBDM.StringAsJSON(fieldbyname('rep_name').asString)])); + result := true; + next; + end; + finally + free; + end; + result := true; +end; + +function TConnectionsDM.ProcessOptionValues(ReportName, ParamName: string; + ParamValues: TStrings; out Answer: string; out RetValue: QWORD; out + OptionValues: TStrings): boolean; +var + ASQL: string; + code: string; + i: integer; +begin + ASQL := format( + 'select source from xp_report_params p '+ + ' join xp_report_cgi c on c.xp_rpt_id=p.xp_rpt_id '+ + 'where c.cgi_name=%s and p.name=%s and p.type= in (1,17) ', + [TNIDBDM.StringAsSQL(ReportName), TNIDBDM.StringAsSQL(ParamName)]); + code := MainCon.QueryValue(ASQL); + TNIDBDM.UpdateWithArguments(code,ParamValues); + if pos('{',code)>0 then + begin + result := false; + Answer := 'недостаточно данных'; + exit; + end; + ASQL := code; + OptionValues := TStringList.Create; + if ASQL<>'' then + with MainCon.GetData(ASQL) do + try + while not eof do + begin + OptionValues.Add(format('{"id":"%d","value":"%s"}', + [Fields[0].AsInteger, TNIDBDM.StringAsJSON(Fields[1].AsString)])); + next; + end; + finally + free; + end; + result := true; +end; + + +procedure TConnectionsDM.LoadConfig; +var + ini: TIniFile; + inifile: string; +begin + inifile := ChangeFileExt(ParamStr(0),'.ini'); + ini := TIniFile.Create(inifile); + try + fDataHost := ini.ReadString('DATA','host','localhost'); + fDataPort := ini.ReadInteger('DATA','port',7079); + fDataBase:= ini.ReadString('DATA','database',''); + fServicePort := ini.ReadInteger('PARAMS','port',6543); + flogFolder:=ini.ReadString('PARAMS','log',''); + fTimeOut:=ini.ReadInteger('PARAMS','timeout',CONNECT_TIMEOUT); + finally + ini.free; + end; +end; + +procedure TConnectionsDM.Log(Sender: TObject; msg: string); +var + f: TextFile; +begin + + try + if fLogFolder='' then exit; + LogLock.Enter; + try + AssignFile(f, fLogFolder); + if fileexists(fLogFolder) then + append(f) + else + rewrite(f); + if Sender is TComponent then + writeln(f,DateTimeToStr(NOW()),#09,Sender.ClassName,'-',(Sender as TComponent).Name, #09, Msg) + else if Assigned(Sender) then + writeln(f,DateTimeToStr(NOW()),#09,Sender.ClassName, #09, Msg) + else + writeln(f,DateTimeToStr(NOW()),#09, #09, Msg); + closeFile(f); + + finally + logLock.Leave; + end; + + except on e: Exception do + raise; + end; +end; + +procedure TConnectionsDM.Start; +begin + log(self,'Start'); + MainCon.connection.RemoteHost:=DataHost; + MainCon.connection.RemotePort:=DataPort; + MainCon.connection.Database:=DataBase; + MainCon.OpenConnection; + Input.OnIdle:=@Idle; + Input.Start; + fRunning:=true; +end; + + +procedure TConnectionsDM.Idle(Sender: TObject); +var + i: integer; +begin + MainCon.ExecuteSQL('select 1'); + for i := conlist.Count-1 downto 0 do + if not TBaseConnection(conList[i]).Finished then + TBaseConnection(conList[i]).Idle; +end; + +initialization + TCommandCollection.Init; +finalization + TCommandCollection.Done; +end. + diff --git a/exttypes.pas b/exttypes.pas new file mode 100644 index 0000000..bcefcaf --- /dev/null +++ b/exttypes.pas @@ -0,0 +1,422 @@ +unit extTypes; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils, LNet, syncobjs; +const + version='0.0.0.1'; + cmdRequest=1; + cmdAnswer=2; + cmdError=3; + StatusWaiting=1; + StatusProcessing=2; + StatusComplete=3; + StatusError=4; + PacketStart:qword=$1F2E3D4C5B6A7908; + ErrorProcessor=1; + ErrorLogin=2; + ErrorConnect=3; + ErrorCommand=4; + ErrorComplete=5; + ErrorArguments=6; + ErrorInternal=$100; + CONNECT_TIMEOUT=15; + +type + TBuffer=Array of Byte; + TParamArray=Array of QWORD; + TLogger=procedure(Sender: TObject; Msg: String) of object; + EFormatException=class(Exception); + { TConnectionThread } + + { TRoundBuffer } + + TRoundBuffer=class + private + intdata: TBuffer; + ptrRead,ptrWrite: integer; + fSize,fDataSize: integer; + fReadReady,fWriteReady: TSimpleEvent; + fClosed: boolean; + cs: TCriticalSection; + public + constructor Create(BufferSize: integer); + destructor Destroy; override; + function Push(const data; datasize: integer): integer; + function Pop(var data; datasize: integer): integer; + function ReadFromSocket(ASocket:TLSocket): integer; + procedure Read(out Value: byte); overload; + procedure Read(out Value: word); overload; + procedure Read(out Value: dword); overload; + procedure Read(out Value: qword); overload; + procedure Close; + property ReadReady: TSimpleEvent read fReadReady; + property WriteReady: TSimpleEvent read fWriteReady; + end; + TCommandData=class + Code:DWORD; + Param:QWord; + Name: string; + Keys: TStrings; + iValues: TParamArray; + Data: TStream; + constructor Create(ACode:DWORD;AParam:QWord; AName: string; AKeys: TStrings; AValues: TParamArray; AData: TStream); + destructor Destroy; override; + procedure AssignTo(out ACode:DWORD;out AParam:QWord; out AName: string; out AKeys: TStrings; out AValues: TParamArray; out AData: TStream); + end; +procedure CopyBytes(var Dest: PByte; const Data: byte); overload; +procedure CopyBytes(var Dest: PByte; const Data: word); overload; +procedure CopyBytes(var Dest: PByte; const Data: dword); overload; +procedure CopyBytes(var Dest: PByte; const Data: qword); overload; +procedure CopyBytes(var Dest: PByte; const Data: TBuffer); overload; +procedure CopyParamArray(const Source: TParamArray; out Dest: TParamArray); +procedure LogStrings(logger: TLogger;Sender: TObject;Name: string; Data: TStrings); +implementation +procedure CopyParamArray(const Source: TParamArray; out Dest: TParamArray); +var + i: integer; +begin + setlength(Dest,length(Source)); + if length(Source)>0 then + for i := low(Source) to High(Source) do + Dest[i] := Source[i]; +end; + +procedure LogStrings(logger: TLogger; Sender: TObject; Name: string; + Data: TStrings); +var + i: integer; +begin + if assigned(logger) and assigned(Data) then + begin + logger(Sender,Name); + for i := 0 to Data.Count-1 do + logger(Sender,' '+Data[i]); + end; +end; + +procedure CopyBytes(var Dest: PByte; const Data: byte); +begin + dest^ := Data; + inc(dest); +end; + +procedure CopyBytes(var Dest: PByte; const Data: word); +var + i: integer; + l: word; +begin + l := Data; + for i:=0 to sizeof(word)-1 do + begin + Dest^ := l and $FF; + l := l shr 8; + inc(Dest); + end; +end; + +procedure CopyBytes(var Dest: PByte; const Data: dword); +var + i: integer; + l: dword; +begin + l := Data; + for i:=0 to sizeof(dword)-1 do + begin + Dest^ := l and $FF; + l := l shr 8; + inc(Dest); + end; +end; + +procedure CopyBytes(var Dest: PByte; const Data: qword); +var + i: integer; + l: qword; +begin + l := Data; + for i:=0 to sizeof(qword)-1 do + begin + Dest^ := l and $FF; + l := l shr 8; + inc(Dest); + end; +end; + +procedure CopyBytes(var Dest: PByte; const Data: TBuffer); +var + i: integer; +begin + for i := low(Data) to high(Data) do + begin + Dest^ := Data[i]; + inc(Dest); + end; +end; + + { TRoundBuffer } + + +constructor TRoundBuffer.Create(BufferSize: integer); +begin + inherited Create; + cs := TCriticalSection.Create; + SetLength(self.intdata,BufferSize); + fSize:=BufferSize; + fDataSize := 0; + self.ptrRead:=0; + self.ptrWrite:=0; + fReadReady := TSimpleEvent.Create; + fWriteReady := TSimpleEvent.Create; + fWriteReady.SetEvent; + fClosed:=false; +end; + +destructor TRoundBuffer.Destroy; +begin + cs.free; + fReadReady.Free; + fWriteReady.Free; + setLength(self.intdata,0); + inherited Destroy; +end; + +function TRoundBuffer.Push(const data; datasize: integer): integer; +var + i,delta: integer; + p:PByte; + rem: integer; + s: string; +begin + result := 0; + if dataSize<=0 then exit; + if fClosed then exit; + p := @data; + i := ptrWrite; + rem := dataSize; + s := ''; + if fDataSize=fSize then + fWriteReady.WaitFor(INFINITE); + if fClosed then exit; + delta := 0; + while not fClosed and (rem>0) and (fDataSize+delta0) and (delta>0) do + begin + p^:=intData[i]; + s := s + inttohex(intData[i],2)+' '; + inc(p); + i := (i+1) mod fSize; + dec(delta); + dec(rem); + end; + cs.Enter; + ptrRead := i; + fDataSize:=delta; + if fDataSize=0 then + fReadReady.ResetEvent; + cs.Leave; + result := datasize-rem; + fWriteReady.SetEvent; +end; + +function TRoundBuffer.ReadFromSocket(ASocket: TLSocket): integer; +var + p: PByte; + s: integer; +begin + if fClosed then exit; + s := fSize-fDataSize; + if s>0 then + begin + p := GetMem(s); + try + s := ASocket.Get(p^,s); + result := Push(p^,s); + finally + FreeMem(p); + end; + end + else + begin + result := -1; + fReadReady.SetEvent; + end; +end; + +procedure TRoundBuffer.Read(out Value: byte); +begin + Pop(value,sizeof(byte)); +end; + +procedure TRoundBuffer.Read(out Value: word); +var + rem,l : integer; + p: PByte; + lBytes: array[0..1] of byte; +begin + Value := 0; + rem := 2; + p := PByte(lBytes); + repeat + l := pop(p^,rem); + dec(rem,l); + inc(p,l); + if l=0 then sleep(100); + until rem=0; + for l := 1 downto 0 do + Value := (Value shl 8) or lBytes[l]; +end; + +procedure TRoundBuffer.Read(out Value: dword); +var + rem,l : integer; + p: PByte; + lBytes: array[0..3] of byte; +begin + Value := 0; + rem := 4; + p := PByte(lBytes); + repeat + l := pop(p^,rem); + dec(rem,l); + inc(p,l); + if l=0 then sleep(100); + until rem=0; + for l := 3 downto 0 do + Value := (Value shl 8) or lBytes[l]; +end; + +procedure TRoundBuffer.Read(out Value: qword); +var + rem,l : integer; + p: PByte; + lBytes: array[0..7] of byte; +begin + Value := 0; + rem := 8; + p := PByte(lBytes); + repeat + l := pop(p^,rem); + dec(rem,l); + inc(p,l); + if l=0 then + sleep(10); + until (rem=0) or fClosed; + for l := 7 downto 0 do + Value := (Value shl 8) or lBytes[l]; +end; + +procedure TRoundBuffer.Close; +begin + fClosed := true; + fReadReady.SetEvent; + fWriteReady.SetEvent; +end; + +{ TCommandData } + +constructor TCommandData.Create(ACode: DWORD; AParam: QWord; AName: string; + AKeys: TStrings; AValues: TParamArray; AData: TStream); +var + i: integer; +begin + Code := Acode; + Param := AParam; + Name := AName; + if assigned(AKeys) then + begin + Keys := TStringList.Create; + Keys.Assign(AKeys); + end + else + Keys := nil; + setLength(iValues,length(AValues)); + for i := low(iValues) to high(iValues) do + iValues[i] := AValues[i]; + if assigned(AData) then + begin + Data := TMemoryStream.Create; + AData.seek(0,soFromBeginning); + Data.CopyFrom(AData,AData.Size); + end + else + Data := nil; +end; + +destructor TCommandData.Destroy; +begin + if assigned(Keys) then Keys.Free; + if assigned(Data) then Data.Free; + setLength(iValues,0); + inherited Destroy; +end; + +procedure TCommandData.AssignTo(out ACode: DWORD; out AParam: QWord; out + AName: string; out AKeys: TStrings; out AValues: TParamArray; out + AData: TStream); +var + i: integer; +begin + ACode := Code; + AParam := Param; + AName := Name; + if assigned(Keys) then + begin + AKeys := TStringList.Create; + AKeys.Assign(Keys); + end + else + AKeys := nil; + if assigned(Data) then + begin + AData := TMemoryStream.Create; + Data.Seek(0,soFromBeginning); + AData.CopyFrom(Data,Data.Size); + end + else + AData := nil; + CopyParamArray(iValues,AValues); +end; + +end. + diff --git a/fr_utils.pas b/fr_utils.pas new file mode 100644 index 0000000..fb260e5 --- /dev/null +++ b/fr_utils.pas @@ -0,0 +1,91 @@ +unit fr_utils; + +{$mode ObjFPC}{$H+} + +interface + +uses + SysUtils, Classes, fs_iinterpreter, xpMemParamManagerUnit, controls, frxClass, cgiDM; +type + + { TxpFRFunctions } + + TxpFRFunctions = class(TfsRTTIModule) + private + class var fData: TNIDBDM; + class var fVars: TxpMemParamManager; + function CallMethod(Instance: TObject; AClassType: TClass; const AMethodName: String; var Params: Variant): Variant; + function _(const szMsgId: string): string; + public + constructor Create(AScript: TfsScript); override; + class procedure SetReport(Adata: TNIDBDM; AVariables: TxpMemParamManager); + end; + +implementation + +uses + //gnugettext, + numberinwords; +function TxpFRFunctions._(const szMsgId: string): string; +begin + Result := szMsgId; + if Assigned(fData) then + begin + result := fData.QueryValue(format('select interpretation from xp_vocabulary where term=%s',[TNidbDM.StringAsSQL(szMsgId)]),szMsgId); + end; +end; +constructor TxpFRFunctions.Create(AScript: TfsScript); +begin + inherited Create(AScript); + with AScript do + begin + AddMethod('function _(str:string):string', @CallMethod, 'LMS функции', 'Функция _()'); + AddMethod('function Log(str:string):integer', @CallMethod, 'LMS функции', 'Функция LOG()'); + AddMethod('function Variable(str:string):string', @CallMethod, 'LMS функции', 'Функция Variable()'); + AddMethod('function AnsiLowerCase(str:string):string', @CallMethod, 'LMS функции', 'Функция AnsiLowerCase()'); + + AddMethod('function ЧислоСловами(number:integer;gender:integer;declension:integer):string', @CallMethod, 'LMS функции', 'Функция ЧислоСловами(число, род (0=М,1=Ж,2=С), падеж (0=И,5=П))'); + AddMethod('function НомерСловами(number:integer;gender:integer;declension:integer):string', @CallMethod, 'LMS функции', 'Функция НомерСловами(число, род (0=М,1=Ж,2=С), падеж (0=И,5=П))'); + + AddMethod('function Нагрузка(reqname: string; paramNumber: integer):string', @CallMethod, 'LMS функции', 'Функция Нагрузка(параметр нагрузки, номер поля (1=не менее,2=не более))'); + AddMethod('procedure Логотип(ControlName: string; ImageName: string)',@CallMethod,'LMS функции','Отображение логотипа'); + end; +end; + +class procedure TxpFRFunctions.SetReport(Adata: TNIDBDM; + AVariables: TxpMemParamManager); +begin + fData := AData; + fVars := AVariables; +end; + +function TxpFRFunctions.CallMethod(Instance: TObject; AClassType: TClass; + const AMethodName: String; var Params: Variant): Variant; +begin + if AMethodName = '_' then Result := _(Params[0]) + else + if AnsiSameText(AMethodName,'Log') then begin + {$IFDEF DEBUG} cxlogger.TLogSystem.Loggers['fastreport'].writelog(Params[0],Params[1]); {$ENDIF} + result := true; + end + else + if AnsiSameText(AMethodName,'Variable') then + begin + if fVars<>nil then + Result := fVars[(Params[0])] + else + Result := ''; + end + else + if AnsiSameText(AMethodName, 'AnsiLowerCase') then Result := AnsiLowerCase(Params[0]) + else + if AnsiSameText(AMethodName, 'ЧислоСловами') then Result := number999(Params[0],Params[1],Params[2]) + else + if AnsiSameText(AMethodName, 'НомерСловами') then Result := number999_ord(Params[0],Params[1],Params[2]) +end; + + + +initialization + fsRTTIModules.Add(TxpFRFunctions); +end. diff --git a/lms_cgi.lpi b/lms_cgi.lpi new file mode 100644 index 0000000..691c5a8 --- /dev/null +++ b/lms_cgi.lpi @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + <UseAppBundle Value="False"/> + <ResourceType Value="res"/> + </General> + <BuildModes> + <Item Name="Default" Default="True"/> + </BuildModes> + <PublishOptions> + <Version Value="2"/> + <UseFileFilters Value="True"/> + </PublishOptions> + <RunParams> + <FormatVersion Value="2"/> + </RunParams> + <RequiredPackages> + <Item> + <PackageName Value="lnetbase"/> + </Item> + <Item> + <PackageName Value="DataPortLasarus"/> + </Item> + <Item> + <PackageName Value="Abbrevia"/> + </Item> + <Item> + <PackageName Value="frxe_lazarus"/> + </Item> + <Item> + <PackageName Value="dcpcrypt"/> + </Item> + <Item> + <PackageName Value="fr_lazarus"/> + </Item> + <Item> + <PackageName Value="nnzdata"/> + </Item> + <Item> + <PackageName Value="WebLaz"/> + </Item> + <Item> + <PackageName Value="FCL"/> + </Item> + </RequiredPackages> + <Units> + <Unit> + <Filename Value="lms_cgi.lpr"/> + <IsPartOfProject Value="True"/> + </Unit> + <Unit> + <Filename Value="tcpthreadhelper.pas"/> + <IsPartOfProject Value="True"/> + </Unit> + <Unit> + <Filename Value="tcpclient.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="tcpClient"/> + </Unit> + <Unit> + <Filename Value="exttypes.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="extTypes"/> + </Unit> + </Units> + </ProjectOptions> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <SearchPaths> + <IncludeFiles Value="$(ProjOutDir)"/> + <UnitOutputDirectory Value="U:\Apache\Apache24\cgi-bin"/> + </SearchPaths> + <Other> + <CustomOptions Value="-dDEBUG +-dLOG +-dCGI"/> + <OtherDefines Count="3"> + <Define0 Value="DEBUG"/> + <Define1 Value="LOG"/> + <Define2 Value="CGI"/> + </OtherDefines> + </Other> + </CompilerOptions> + <Debugging> + <Exceptions> + <Item> + <Name Value="EAbort"/> + </Item> + <Item> + <Name Value="ECodetoolError"/> + </Item> + <Item> + <Name Value="EFOpenError"/> + </Item> + </Exceptions> + </Debugging> +</CONFIG> diff --git a/lms_cgi.lpr b/lms_cgi.lpr new file mode 100644 index 0000000..08b8089 --- /dev/null +++ b/lms_cgi.lpr @@ -0,0 +1,166 @@ +program lms_cgi; + +{$mode objfpc}{$H+} + +uses + Interfaces, Classes, SysUtils, inifiles, httpDefs, fpweb, custweb, custcgi, + cxlogger, abbrevia, lnetbase, tcpClient, tcpthreadhelper, extTypes; + +Type + + { TMyCGIHandler } + + TMyCGIHandler = Class(TCgiHandler) + Private + fAnswer: string; + fMode: byte; + fCode: DWORD; + fParam: QWORD; + fValues: TStrings; + fData: TStream; + function answerReady(Sender: TMainThread; const mode: byte; + const Code:DWORD; const QValue: QWORD; const Answer: string; const Values: TStrings; const iValues: TParamArray; const Data: TStream): boolean; + Public + Procedure HandleRequest(ARequest : Trequest; AResponse : TResponse); override; + procedure log(Sender: TObject; msg: string); + end; + + + { TMyCGIApp } + + TMyCGIApp = Class(TCustomCGIApplication) + private + flogFolder: string; + fHost: string; + fPort: integer; + procedure LoadConfig; + Protected + function InitializeWebHandler: TWebHandler; override; + public + property Host: string read fHost; + property Port: integer read fPort; + property LogFolder: string read fLogFolder; + end; + +const + aTypes: array[0..3] of string=('"UNKNOWN"','"REQUEST"','"ANSWER"','"ERROR"'); + +function TMyCGIHandler.answerReady(Sender: TMainThread; const mode: byte; + const Code: DWORD; const QValue: QWORD; const Answer: string; + const Values: TStrings; const iValues: TParamArray; const Data: TStream + ): boolean; +begin + log(self,'AnswerReady'); + fAnswer:=Answer; + fMode:=mode; + fCode:=code; + fParam:=QValue; + if assigned(Values) then + begin + fValues:=TStringList.Create; + fValues.assign(Values); + end; + if assigned(Data) then + begin + fData := TMemoryStream.Create; + Data.seek(0,soFromBeginning); + fData.CopyFrom(Data,Data.Size); + end; + Sender.Terminate; +end; + +procedure TMyCGIHandler.HandleRequest(ARequest: Trequest; AResponse: TResponse); +var + clt: TClientMainThread; + i: integer; + k,v: string; + allfields: TStrings; +begin + log(self,'HandleRequest'); + LogStrings(@log,self,'QueryFields',Arequest.QueryFields); + allfields := TStringList.Create; + try + allfields.AddStrings(ARequest.QueryFields); + allfields.AddStrings(ARequest.ContentFields); + clt := TClientMainThread.Create(ARequest.QueryFields.Values['action'],allfields,@Log,(Owner as TMyCGIApp).Host,(Owner as TMyCGIApp).Port,@answerReady); + clt.start; + clt.waitFor; + + finally + allfields.free; + end; + log(self,'Data READY'); + if not assigned(fData) then + begin + AResponse.ContentType := 'application/json'; + AResponse.Contents.add('{'); + AResponse.Contents.add('"type":'+aTypes[fMode]+','); + AResponse.Contents.add('"code":'+inttostr(fCode)+','); + AResponse.Contents.add('"value":'+inttostr(fParam)+','); + AResponse.Contents.add('"name":"'+(fAnswer)+'",'); + if assigned(fValues) then + begin + AResponse.Contents.add('"values":['); + for i := 0 to fValues.Count-1 do + begin + AResponse.Contents.Add(fValues[i]+','); + end; + AResponse.Contents.add(']'); + fValues.Free; + end; + AResponse.Contents.add('}'); + end + else + begin + AResponse.FreeContentStream := true; + AResponse.ContentType:='application/pdf'; + fData.Seek(0,soFromBeginning); + AResponse.ContentStream := fData; + end; + log(self,'Sending'); + AResponse.SendContent; + log(self,'Sent'); +end; + +procedure TMyCGIHandler.log(Sender: TObject; msg: string); +var + f: TextFile; +begin + if (Owner as TMyCGIApp).LogFolder='' then exit; + assignfile(f, (Owner as TMyCGIApp).LogFolder); + if fileexists((Owner as TMyCGIApp).LogFolder) then append(f) else rewrite(f); + writeln(f,msg); + closefile(f); +end; + +procedure TMyCGIApp.LoadConfig; +var + ini: TIniFile; +begin + ini := TIniFile.Create(ChangeFileExt(ParamStr(0),'.ini')); + try + fHost := ini.ReadString('PARAMS','host','localhost'); + fPort := ini.ReadInteger('PARAMS','port',6543); + flogFolder:=ini.ReadString('PARAMS','log',''); + finally + ini.free; + end; +end; + +function TMyCGIApp.InitializeWebHandler: TWebHandler; +begin + LoadConfig; + Result:=TMyCgiHandler.Create(self); +end; + + + +begin + with TMyCGIApp.create(nil) do + try + Initialize; + Run; + finally + Free; + end; +end. diff --git a/lmsreport.ico b/lmsreport.ico new file mode 100644 index 0000000000000000000000000000000000000000..10c5fc1a3d8d9ff264229f4ff1cf99ebc83f167f GIT binary patch literal 133345 zcma&NbyOV96D~ZvEKXnv?h*ndxCD0yZXvjYMFT;DJBtJl65KUN2=2ia2@*VL(8b-| zFYo<*_y2Fs%$%t+Ju}aAS9ev{Qw;zh00a1M0|B(acp3nNJYB=#|C5<9K!9F40B~~t zPcD8500H?RfSLJ!vK2W1yy|~?lJ@_*4+Vg@RS-Z*`ad}g695FyKmZ}(|KvtA0O+^} z0nn#%|9#I20sx*6Fc7Y;D)$VF9P8=R&lKckH2%B$-+}Q|9x!w&wgjFEE67M{dSvXi zx~EX}x-#7ePcHZLBVf>vvGNRH21z)C=WVKHEt_7#PXv<TR@A@R*7su5$opRV_MhTn z;?K$15x;LitmD<pujA<1xD=q6mT(4)Z@|-o81Oz)<t&O{yZ7&MOUGN?IpfwSeMp=g z+_f&Zf81nh7X1j&OZ)!+Yxj!8SK&vg;q}{Dyb8ar)Wy4=K)E^<$PJR9$}nRGQ2|20 z2M7SI0|5et0IFv=9rVwNz#&nn6%=6&%>-Y)!u3lNP(|4j_;?41JR&-%8fY>*do#W( z<miEp7;`;$9oPxEr^pX(DSSG;z{Z&BcyMY!FVD-UKlhh#8T00Gb%_qa7dcQ18WIR= zmlE5j?{SO1If!)FySi~)2u=HLU8D!%L@C*vovWTOUg!=t8pce9_M$<P2=K9!*DyfY z@o4<$K%PAMG`d@D6hd%XP5@m(#7WEvb=cQ&Z!<WvyDAeDbE9H=8&I}HS>}AImu8wq z1n|JU*jvhow9obF-H6cZff;$3ay?m)fn?eXKp4M(HWnPv1;v5n-OL&r@|@8KFs%xs z%ipc9M546|{MJ(NWp%L|9sueI##ZMq>pN;1s67tSKT_D`>!ndXy?tCpiErUo()FEh z#*r$xoDbv>m?;g{qa=Va#)`?Vh>yjcR0uHbrgS1?#8ERHU>~T%0zZI_1GAJW<2|Dy z^D~s*(Z*mBuSf+rAK)(w;)?Y7%%AQ=JBX4mn<f>)Em0AKV?AH9T~upM&XEr)z@oH_ z(T<^fbNt_l=SW&k#tvniC@`D{({I2HW^r2SjH;UoPFaNu>K#wJb!6bASfD^S2Li67 zV1r=bstx^$5(qui0&uTB5cxGh+Vw+>Uv*U^A$49CN8@%-P)WJEkowhu9dWar9S^!L z6FK9S>eSVxr1Y>%0%&)R%pkv#Zyz$z)9^%t%M422kFq&<Kh;6JVz$KqUz=Cc$dw9m z?}bbXQ#AH_BdaTkc%Bf6W*$s8Sr{#}2Dx_Xh*jo@#pZ}LRLPC1+XS}lMa4%S59}bM zEkw<w0>M4H!5C2<R$p<i0HpJkI=2)kOT8ok`4y+@37JJPO=50MG46RTH#g3K_mN=e zp%NVuTM}twf(#9erY|m@r&t^iJTU1#w7ql|U#ME$P3+(M-oP`21y{&<ne6BD>~zK< zXloU+LnwzbDxH1^;1eJ$RyfR7%s!qt8f+l!w&Cw>gAA+!_Ge%aPT5MrQL>J2Qk~Vx zkpYN=W<($?r~0+2r~{lDI{S}b*=Ol$|9T~*7qmn|dx?xZq%78c&5U=RFEkpd@o1fJ ztJXn576<MinLbW*JgRdAx=cqq;j01xkIjdt{vua{?fY4Gi{9R2KY<k5ou-R=)@L4v zm*HalVddzx2Gk%?>l^|u&PW)3P&^0yxEP-ODW!S(+!sNP4m*Wh7+0Vj=8-{^wIoar zc%i4N_gtnD$2|bxB$FG>3eoPMpUR<ska~}OxNs>b@`|$Cy*%K+bERPu8|F<}?6p#{ zi5Y<GznP4qi^9#2yxx12;1!cEB!V$QViA0ckOnio##wt57U<=d{d0G*I`TL-O6Y!U z@a;(#1+4oEmkfK<uXzO)6lK`pPOF#_fGkLv@eN;)#av(Q@Y}*Vf`3z<eY_2bY79Oz z@YeG^KC?<zgsJjfec)+;gnB1XnKF8%ycjQ!feubif~T=zh==~|{7b{n>z8wnB2juM z_9F{uy!mkM;~^$DdAaNv>o54D?aD(ebn}`y1}ER;0wU`jV|F&kb(M%EGLjtFGu?@} zeCcdHEOb<Zywb_mYQMM{U-EPPBXr9qUjs%`TKE5MuxIEI9|{x0Lg1!fL1D@IsbL+} zq^!M?<z^r!XA-*cMa<rM)KNQZTol{%4#q$;1M<D--$>3tI}|>G-Zw|zIjf$rNa}(y zOhM``60aX&^jyk>GFRVY4wb|glEEGE`m>ji*wPGa7KwmtXAiCDh{ODya5JKq$o96- zPm?5ph*f=EO<$VCAE6qDpK-E9qW+fTAN`Oby_yd_{CtH(a(9weV+r^SU#kgt8hiNO zwu=kZy+D>p)_`HQGNKgX$1E~?F_JE*Im*^1?|IOkNuhepeU9DNh2PD8>2q=EsG<@e z?x+fub+<+9wR9v4vg2voN%@V`$Ejx<;C>Dcd<T044Tinr0ue(1HvIeeb}dKz(Nst{ zF+2rRJqdm^2C{Dibsk=Rso<z<=DYW$2DODo^u9ax(frYNH@xnC`T`Qptn+sJj0-1Q zAZm=q0a2>`UHDz^Yk(TGX)IQGZy+8LWi+he`!pOp`&+Hj&=QYg46%9vJ>UPnQ76MW zVu@q_p5-KSp^sxLWYrQZVSp2@(x;0PZPrnTQEu1a+U?m(?@3w7ri6jAj;l*-i(UED zLV*%F7IC-_sql{)7_l2VQ2Y|jMQDi+!2SiE1wumgb<yq<w4hGtEeLvbM8yqdVnobS z#TydYOMYL;-@9hk@l7bDeod`j?#1ja_F9V$@<W4DbASjZTPSe(MMWO3Bcm<RmWL#5 zEu{GYdvMeLdX}fbAFn1bhy7{L5URBuXFEI-6qA!l4{#*R?C%rR1D_4LzS}a4lA6{q zH<&dsP|PPV90;`GY2=fZ1ua_k39K}j@t7BE=;#}#JT8;M|K2EH-@ZnCwDLFENAD2R z8296D9Im5)34mqwX>=qdQCY>I0Hp?PaZcvht}hw-k$`%clV;P|eWK;1kKMBbmKFD= zoqWx6^>Pf*yK=9S6bIdBopz+#!a>Vs=$t+)t{&^vxFR?6=$D@Ij^VvXHT=7g*82Mk znimI$gWn4DK(SIv98DHAcs{B4Mwl`IYB2KvJt+d=f8Yr0q+9`ivW%Y-7kmV13yhd9 z*ktK+tNMMq&#$7(Mw*}B?U>C$4D^YTaB$LcNX7$X7vX=NC;Z6r;O$_}@F5@oVpF|k z0-);Sfq!3)o?hl4C^sqRNq$UCcS`7^`#}jckvia9Ln44f>pN}n6|)u?^wbPDhHnk9 z#9k+IheT6#k1p$|laa|x5`Yn#)OQS$$lDmGr~lpTg@gE4JxOT39tfKIo8*22Jn-Yr zo&I7Du3R4pHby&`FXSU6DU-={#y6aM>Zy)Yx%)i0(o;k^BSQ68`(dl)FCl?8k?`!3 zTRX-TP!bNceo`2Vu23GwDh^zE!%Ra5eY;I^sv=EplMV!BOl66ZPh)RX#i55BNX26P z)IsS)54h#l#aPb1HT?Z-O%4^y_r=xbXX^{>edBL%jzknXPW@x`AM%pw$L}<>0|>FY ziWU*b<yPkN%z#bLS$cUWWPbDRR+H_qKVY-X8Cnx4G5!;Xt##X1IZzk7^bZ&}ex9oU zlT|I@3WjG}rv_~tO!n1{5CHWG=_lYVzG|$a@~ia3*(I&105dCZvg7G7A7@o#CLB~x zi1|;jW<Oew7mYP21wGK`j`=v4BahK~hy!d-_#8(c!)nPu|5+OcIPA3*^ZUS3ZpY$6 z(C<Mw6fN;bOm*+O@-SVAlIQpU(Xnss^Tuc2Bb*2XWQQrc5FHCo=w&BQ5xz88go^Wc z->LCFANYO`Y~yIFTJ3$2LIkM+1ArIb!Ug-)!xBM0^*-G|lWPx#GTI@xik+2yHP)9V z?6*0?^|4h7z<gD#E8DX>!yYMZEQsC195LlHAN+ojanH_P9R!K<<!GVk3&AnK;)KOf z=~-D%l_6$|B4y`u<g)ZKcZe4BRd+|-SWYk~#}v(^jKvZ!&{|kc57n{-5n74igHKbe z{T5zXU<x9?QzX42E%9Pb4A$cOv4XA$J)@R;wBg8`_b1mH2|c_P@(0aN=Y4g4d~<N! z?tk>pzg<miVNM&`Qih87hAlSL!#zb3KEnhu`}H)YDqUTt7V`04{*?3^_93fECN~OL zAVQ8bC*85#d7BV%zG7zBvLl*#u|HiJCFXpLmveGO_ogMdsea>Y`FlNIkejsTd&cgU z5?2&z0u+DdLZiq?eH_6|T^6w<SWG0Ssb3`7LF&gMnm-J6A%ECnk3n=8KS<fI12{F_ zNlE>}%yN7dqS_kJfx?l?>!__uo%Kf`YKYV)`|HL1gAK>L?{kOul1KGKfD-?r55dR1 zLyQ!PuX_ej2v%%+#>m2hH_WVtXw%w$BGOaD@R9A9uPp-wsIV8SQ10kTU_zvQ2!jvB zR%zqT0D>J*Kka<+O&n}pRuPTyLtK`?^2%kQv7d%5?jb+qCx$JW>W%kV3fAkiU2K8N zXQdLq(8XL3>Ut;^U2LRfSEOv-q$QeM`o~-v7qxKa620JvnC1A2e1}K&s=zVq6_h{R zadxiH+b)aHvzqP=pjCqniBOL@O!)U?KbI_H*M~SRFPfY{OXba39>#C(Sf}2je_M%D z4T;Ok5NyhKK0SO02f9PW>?N&2vG^(wW6>dW?VO@RBr&mQV+#e5zK}O~$DqhJ8Lw2r zA#!cl8IUHAw0?&QsY0=HiaeYbD6u5-+2$)=@0z7YI-$|g=?duMcXY-n61}ik#)?W1 zJvRD{r72=k5FGHro6Ez4<45AB88(SZ`mxJ<@4Jq+CDF5!h2A5+YP3adFl@WWzyavu z2MzKQc#;xG3Rvp()w$=o{zfy880wozLUS87>9b&6qeQ=s`r^|grJDNqcix?f@wv|) z?Ps~Q%}7nd%uIAJ*LHMot%zF9n)C#;*yb0Ch;|`yj!l-sL^UVz1+(*fEv_b#Q(hF8 zm{@CVPf}qWTT?)`w3CYTl@1p@8;FhYwN}C(%KR)ST7J;|*3zSh><HzT&FjDPobllR zZidYFyMC0B`_MmB9s}5c4PSt*|6IwC77qNGqMVO?2~!_o#6jspc^2EK%&c1hHe6JL z1hXGfuCAj-FPfshX5gPWQ>Ct%#ho3Jk)qi4xO|KLkdC*NaR>L{^k?#yZJXd6ikB2+ zZx>&t4xd)I%L4~(6Ai8ig}@*r>x&Fd|EdN#&+v62VkEohuiNS|WLc<Xm8e6W&WhBs zuc<=@q$G;<f}AFMkNWvX8xF}dW#y39St<2|O}{%5a&_*LA8LBV$8UFnP&^{B0~%TY zU9V%dUBUcAsL30sauJtlr7hZAb!>VKw2XyAhEM0o&<IXTeULppC8O}1#)80Y!e5E& z$X-Z~dvuO4d2+}S@*Q`@wQx>?-KbQ2%X6Z2N>}Mg(J7?9u&1wdA04@?zWyZd6iQcK zgZsrB@G#uKCmj#{6ap(?MU`lZSgz(^4gBUvpjlbTD-`jd<8E>_BdIB>_6yaInGbpE zfteVPjwkjQw29x1w&)ayNV7*{jQ3)1I?wOkVy9=ta9hA1^L>KbPc&A4qfvYDQ<6Pd zQp_wjw(8xWfYtCn<_${T&F;&MjG)4wL8d*#+5s*gm!9mG$y<MTjbxRX_9k$VpG9kx z(|HPBo(r0F;hryZ4fa%+4Nl9A$l)=b!1Rela?3q2sjmx?o>k+lGFi+^9O%oyxQ}1S zyIXR8E0}oxNn_x9DzrOl`>jUOqUy6sR`k>h)Jp8KV%!Y0OHnEan&u+v#<$F-x+drI zsx~tYk?hXx^sL1+Fml#rKdVW{KP&734bH;q$8~OLKx{iF<#zyfF?4*1RYs-JJjZNl z$Gc9+yWYJ7TuCt#7QBfa(slAuV?W~JKj5?^ggcVkCv?(cb`6OX#gNe$F)#a^!MKZa z)%b9BW(299%P0^l?{$5x7SSJu@@~AXrG*niys}YDHK3#c+ApE3SQSYLH6rwJSd~t1 z%{N#>jL8GUg-DNvTxTXNlkOjq%IhNMe>@_Ojubd@!xtyck_Z46_M5LwLmy6!Aiv1q zi^ggNQ?rgnBxNk@GP{4C^;z)rH4{gWQT+b-LL$3vEP({+@F!yRNh-RwHK&R@*(FVP z3R9LeOJ(Dqo|mKjXeH;1^Um?;=y()Won``qMp?@UGl$QH!Fr1U`#10DxJR@;q`xIy zgsk%`dYQs}3Ui`U5-2Z*?vy`)la{Anu{=#vn4u&CZ%($*OzfvEXtM473AIRq=G(zv zefHbG*ykqBM@P5s0}4E_M-3DK7h|V^xAUD{5d8{XNW5i}-CSRNZLZn97W1GHE!dlk zrQkfhhA-_@YL(xDijbQRE0uO8xhg2ps{@S^h<Cm)MFa=IxEv|Egnth87L`c9_58Y< za>t;fHn#P34J4!L48e~0dyqdcxuGyZ>lpk|^=wBDp5jEH92xbSn~f}}uVb`B53O_* ztHDmJC4lFs^ra{Pf%3>-(*f&WCD!&MUH84lVsVI#^lyvY{~HczWgis~8R`j3&z(<x zUoTi0d!wFQw;Y`tfgVh?udmBF?`v=)^bF7+s>{nrj%J91-YOqXlxn!-)@hKw*)A-< z(?Xvpgv+DflS<f~6pXyI+Gp5hSkALtnAr{v>Zn_J{<@zV1G8^5Uc=G%i-vOtVTtH- zIx+KKXdMsXkk(nHMu_T@|7U-sh`H!!%t<xiYM2wgw<l*Hq<ye0eEgT?c*^T^%s>iY zUGNxi2pVeutpzh;-1+vpjaA!<>g)WJ8+MI#W+N;JariR}zS5&m5v0SxBEv=%oJ@|> z^NGe`-9~0zY}-IC1!C~b&qcN7-k#N&kTE1u5)^+OC<-L!glI%;l;@YAio0g920qSY zM#Wo$H5Q~SUZ#)hZR}x7SDHjFa;yBt!=cL|(cwZX@)cv{kNKmQO2_I>{VgeE!1gjo zC36$+jK=%39N5O;wO2ege5Cq&`maZ(ur|OkH`#(O+d|LL*|WB6E^q@?)Jb*en^-H> ziGs8EW*BWY#oBo@AL`uacgRhaM1p4r-_xtlQ7m$8NT<V-BCY2&cj-0wAqD1+j0-$o z3(S=SZ(Q@vp(fk`BgHUfB8iRGsLk<@(p9AFy+KeksoTiRl*B~Z;K11td@qadTDD)t zxzQ7AdOJjy8W{n{9@fo}4bRf_o9Nb~$gB8v&#fUA!2VbNPXDue0{jODxOBjhsc8hq z+<Zv%v+11CztKNS3om5~+J6In@29@K_fE=MA~A5*TlT$x_nEixzU?63brj-MmN=5X z9_`qALr-{d+KaiP-@<f4L+rStX1aiO^%X1>9s{lT?3s6nN9y!13J)4U7lyyJKOO)( zPGrp=#N1e09@fKgKoZ_LDHsvHMs}t6S`W0m**yPB{qRawZYhBW;-CnseDbyJ{Wvda zqQ-iIA1&=0*ov;Ei~@YbLVpgZJMM@5bI5<SoA>HP^rpGX(&Y6m+zw0w-}2Z($=zlx zNSR;J-10bcD5g{!Jz1uIkM}9dWwEjT9ixw2eD>_!l!}-lQkrf5+0|$<$<liD$GS8P zKwIEM#t;jIm>4aObK+43sIjGk$lfe}C9j}I{;{Z>;%#6x=n5TqDA3%w)JU`L`J1i^ z{M0=zQ7-9k55o6_{_Vx4zsDKEPRh5Nv^1|NKf8R(Yb<l|yTy-dGiOV`@8g=i=<Iaz zL+;?hM|G@hsJ>h_7UinICKqf1;yNpsul!uS2W!XI+kKVA_y+rK=<&|q@d)8(+qU<K zlSKrF9M$z6!f>jj=smfTjP9>-7~h8;`oRn^LzgM%mR$G3S(zom_-KbF%<Q}Za0yUh z2Yf=E?D*h{K16CZb^WjYH$~|p(qB7dSeuu^0qpf26aHVccjn9AnD8x8tUJZix%#gg z7Q18Bdth@OQ#(I8v;0?P16yrp=QZ?w$s;qS=UFeDuhwsTOd*BnFUe3+aA4hE4>;Aw zvpXhWhOB97f;56qK@th9XEr-U**=i$n}rzI5=%1|w@7gE<z8pGk+9>WD}Sfkd^oDB zvYFiVGF?{u)mN<GS~rEYAnYv`ewEux3~l4rDh|IqA=1!T&Py$$W>;znoWy|l>XIMc zcK@{aSNJ7a`}MjvOIMLnPYD?t#v(5mwm+M0m{sR5-)*lRX`RA%YgfzO4jqcriFo-d z`ioT0^KcPClao@UF&)eBN2;pym>2vP6AphtVB;mqwGsqpt)!F=Qy>g1DN3wz-!$@o zzbJ6xReUz~BDC_p->&f{&7CfOsK+BhtglgmYMIH0{W<H`B_WqMeV^{1Xt<6|EY%-u ztQjG%I&zk_b65da>2K4(ULh_gf2h;aXJ6%oNUo_&hb(vNI-#T0KS%!}I%cDYzz$fT z%FnAznKgX(aIOn#MBjFDTD#pOBQKk7#Luo+=eH-(SXbe;V#|k{>*b<ky;y~pQuuxV zT^UWGJ_>uQp{|MwZ*lxVYGFzC!m+?b`*Z*Hjgt7@<25sb&y&K7LAHD}%OAm#nR!+5 zsAf{E!J!W_9>`aAMQL<1#WLpC{eiO7K+qr7gooBUV+K?-l6veiSQBG{cg!xj&Lx8- zTmKujdCl$J_6Ch@o~2JBFTNlKNdW!@bBp0>f3qkt4AOtTQ5|lEwPlM@D=jZgmCB=z zuG3xgE96(%eswTXlbPMx-D9zNmeN{Mo@sqjA0};mQx_-n0dK+znE0jNVqa^jVKnD< zG<xs;(h=kABCB&XMkK7XycPeS4_#a~GD|jng3vEl?%Cv5qlOUJ1fb?Mr74!r9^?i| zP))u@F;v-+iUHPf)g7OVD0aPKgVRvui3)S3Ah8)dWisBuxU$~i2;rY+$cGZdqlz); z0+LWVO>a{THh|N5cA2h-dhgrR=ZrRC=p|!jFA<5g>9@Qe#8i?fTbkxrtc=R^1VqUj zACkGDf>Qdj&$gI<OBmrXprw5Z{DmeDcX?K9&*i^tDsnYWW9pbAo(mbBo{nUqo{M0q zXu@wYq{}p3g(=gfyrmamI%fDYVc&Iju}Zf$xylnO6HHrflXeR_dK<`jHba}Llk>h6 zv$A@q5^LxQJl2&ZEH)^mHLt|(-^qg+`XRbyIW%SL$!foB@FA0F$8!{&{Ag3-&l7=l z$K5YCxZUjK*B)CH$A7UpEqK@SsXFY_+j+3Wk8q0DL48OikgJPhG|7bZ-An6F^!OZ} zCso-+VoFQ@cszXOq#IY#C}Ialn4V448G)(A!V>tfGO?E`h_io)x`^gWcr?3`P6dG5 z@u;9^i0Zd9Uo5RLuTEw^&5r3U@HRA;JYFfq(6%fgqwv8z+E13+<@69fv-u@T`}7Vn z=Qir~@Y`x<M^+a61W$cXpV_iIFDtL~x%b3fTgHl6z&{$>!c4&9TtS*z1Fb-WD=|kE z<=t7%k7k$a>#J>1cT;n74C{#{f8sg6&ou1qC&}u}d}?gP>){?0j2P>|ztUogCFGIs zhE5zcaywwzjQ5}Fkjccjd3Xv9ECYN27AMT-)X#)eFhK*Y1*26ij>zt;O-__Ah$uo} zA$k79p59FuAch-TRU{>n<Xd-eo7+{%8F$p7Ue(~HnIBc;DEm!I^=MT5#U&2J&~Os9 z;8Na^$nwllg-Au9)v{7(RYZJjnD$G*7=?*vdF;{IHvNZ?_vHysTu$^!hPtR^-_cQ1 z-%En)s7VHCnEDpE5H4*{aD>83_C`q42#93~$El|Hj5^4Xt8rM~T>B!;M`^k(;Qj5T zNeE^D858h}qiLN!Ov_hK^i5ayl+n<@RKDk!qB$jF(Ty#kg<rekHd`OeSlfOE|K~p@ z>1hhp>^%=r2Kpf0vQW$C#nQ>h;?k1jGTort!&ZX){H|AOOjeFLcrl_U0DJRE=IryZ z4=#BURF#zIuB&#W<rJ$ZKkS~V;Ns?%S+oJ0vWpG0-$>p}qgc@aT=__pN-R4-ulOu; z!nZ^UW02kOa6_M(=0Mbg5)l=SImiqeZ9OJ94Yy)JaSaoQnqTa%9<8+!)K4mt?!iC3 z@@&dzP)L6a*ckb--ux-XpRve|8OQgnwFKXnuBK=56R56)GTSYaXs-7#Ge?3sH<&Pc zz4&!xuyBaARV7V~sFOH*>m3y{UtJiB%A4X{jCJ#lzVqyfe^JmEvPQW3#3=>O0|%=! z2Kbo<KAZBpA}W&Jk?kd}pD7s_3D5b6s^2JBADz)!_K5}Y$O5+WR|+44TbjeIGF8v2 zFp}(Eh<_B*A8v<`^~w(8H=q76^1&*XKi8Ub`AAtMmtAtb8u!pgtFj$f0=U|8re|}( zg|r*c#3>Oci8e=(=>Ab10c~rc{8k=Tz<+hi=g>s`+cdJHUo%Lk@snX6!#oc|fk(!O zJXN?4$d875L7X0K5W&UI7sz$;a&JSh80Y+n6C=D_|M8l|@+31|=_B5PQCZRts1$8L zdPzRn3t;|>P5YlPF&xrxv7|4yRYZ2X<r2fL(&1vShZ<rDrX*DD=1o|JVi0@NnLW7# zzVzS3w+ZsdQBIC>r;pY-?nr#@9opfc2e<h8`ofis-c5S0iV{?<j%+F_QMoho5xq1` z;>)%fA)#o;-Sy~I*C)F{2-u$Gd*D;|T;-UGin1)ZwT_l*dwWL$s;pD&uXe8r5PFCG zu2j>dz?~wd&^`qv%uT1<)x_ZOc;S;JOn*t1_XR}avd2J7HmZ%`8#k#u+Wx#DXrJW6 zS6jv+0^f*o{VA3qzv;i2VxX&_A?h6B(+NPE92JGz=zX@J^K<_*Z+?@*tW7QvkuaVj zwUsJ4YQ&rANA3s}w)a>&u^W-JbZ%<6<~4#ME=zb%tfEb=lth;_ipO0XdZnjQwJ>K~ zj&_|{PWiNdS~mIT1X7Y?H!WN^(0&aU(I>Jrlw3+@6(}=e&K3e6Xjw>ls<711O^ZDC zAxnI{@w-t9-(n~$?n`|5f4Kn7{|jW$)gQP`L<+K5b~xGARy0#y7xHl}Y+~&j4~9|s zVmZAr2K-n&&XcS!PQ(7ioF$cu)4o8gA^6)LEJT`LP`<nL#TzQy*KIHvPY4oHI8B{i zZ;Qzs5sUV}@fVM&c-%M6HRPKvn<!c~F~PKL*%Rubl5gAekjP{uwfTrL<<KQ?_9mMS zcT#2jlf*i%W4`H{5S;Ve*K>FiV5Rt3LiPQ9s6`%hS~0xoOKGZ|9mT$|7nT%;a^y?S zGH}j+EPU}%HgqgceRQOLwAu$HB2x(|q}=Au5q3i|v3M8#<)>Zbf;TH;@`*;}){8np zl>jIPhUbr55GJnXYa*X*jz{QZMCu7!q+|51!yPh3lB(6=MUH*<-Ng^;dNCLNhYf81 zD4K=q5do*Ix^9H&3*M)uV`>nEE3voY{w|!dMt417Rc__uB~@M@<6IB~D@mwgL-~y( z*(0vu9-vn;vz$$y@PEwNiIJh2J)pQ%%ejUq9Lk3(9Wu5TwwTJPv?<!4(W$Bj|9C%z zq9-zd@R!$UwWiEn4X(6gTVAPx?9G=Ej;WL89oIRpP!ghjyh5*u$C5Ie42<M(D;zTk zCxBzm2kUadS#G7~{5-1zUHoJY7WMnU7<<A5`lfQ{Yf;8~#0p|jbY!*SVa5IPlIW7; zlZgz!i%ygcLI1B`RKCLs;1nN6{e&{p(~%~{tj}~tDG|#-KOX@5Zgtc-1<MT7XMwR( zG3oy1)09JhM@8&FK0ea(v);C<)c48q*kSyHzp8^2ssLxUzxIuQHpOlQrdwT{)D7+0 zidLx>i)oX-(*5QI;LcO0#cQrEsP+0c@yH)lY`!3QqM{!`ra5>-W3-BYC-5HkKJ58y z9CvS?w!G#ua-*8v984!vX9{3;F}hwCKm0{y{BTE;dwJF`Bi1YZ;)S%w%Ghm{$*Pvs zC;N1=9N89Qvp~u>vmC^Sm>V}kmn2TZ&+u~&N&^E{TJXZAzkbGTuES1T%<q^oJqbw7 z)Y~<m`#^CZdPLqS1yi)rhae&%{mjbM9(-jcWN0OB?xS~e>=v~8Hk`Rv98ft9Vjb3o zgjclYpXL$TmFbaSf8JIS3=}~&Xu*KwrP?1jVfO9gX|q73zuJy7`_6~xQypW0<(%j# zTq-`lJ7jdls=?Bgjidib&-hj!G|OX6%+PV!!pbh4$2upv!Nlv4QB%dL%Okt!ysok7 z&m+H3_yiXgwi~XasubYJegX*#cr)^LyL%VeR*{_@);G*&w4rr#z=3!946HsLq;TTW zX{`^D81FoH=aeNuS$@^Nu}+f9S)|mKWYCiFzBS{;p-@LBbvjblwA@(b98;0&Ha?Do z-HbRcapzgz|5b3;6RRC(OJ`3*zw>4$!if?QYiRf{HL(lYwuicr!V|tY&DUR%RiXZY zTD$C$w;7)JevD`nU+YXFXyvZtxYb0A<mC=%h&~wlzkz^AL_2)nh|1H>p|Vn}FRq4s z?*LWW7LGRMJ^59a(z>9X_hxIVI4VuR)}qU9FXk1;r?NL(RsQ~N!#=g_%|JO?yWvXd z;_z;9!zt8IN%qd7*!cUWAL2rb<Ya<l(G}vfT}=5XC!tICXlOShg<>}TOJpsN(|#y2 zwo($AywATCF3ZY=95;zSvPDHl7o6~oX_h1$cXx^ZJ~^E-Q{BuXL)o#)p8C|q$<*(Y z)?sn(vr;i1l@Q4-Mq&f;u?8_WAYRw!2~D8;n{zf|fV%fyrb_iz<?>YQcNu5_tn)rf zp{VmJGN|<OPm{ypjj^@^MRRUkZa(SZ#J2X*+V;P9Cu}V}UVe6a@g^7idA{Zy`Y3-J zHS-GFR+k>shOBEftO;$V9D3$GvCT**S&CQhx_v*nK6GTyhch+2Nka0(X_jdpb=TjH zwz?4gB6hLyD<qgG37lsp5VJ8HN4{Yg^{7nJB*OGVmz1Uh5>TOcu4$XFvo({e4|>r? zxjn269ty5k6TXRm5wwNO)STbJFxBZB2WS-=knG?}^zN~KGCj`5J|0O@*j{q)j605W z*UXFgvl}yfUIg?Ocrc}JKG%?akG4j&bVGh8^l-i%FZX%f^)RR@<jzncvs*2L>GR6% z(t|gp_bN=KjWN<^q-#7z6kh&CRJYQ@Gex0$PMyXM^50}AEJ(=9*q1ifKKMK2Lt}>6 zn<5~nGq+9i&gsn?=1gzPAScfR)mD=;L39Ln>dO4^1A#YnKp1MuHK;D+OfX)Qj6@|b zOQoST()n?fUWTc*;Wz8r2AgcTuNb8gEAq3&nfPv4dk67{0^^bIQx>;KT4Vdai2lZO zy3Fyg{9Nw3^0gA23i7kgVWwka@q3e>`p#TW%<hwRAU!*T8BTsPy0C4@j{McqZC=*k zwET@`n2}A+>Wth=4Uf5s`SN2D>tUJ2A8Vv^rJ7}x3mX3|xKfA!)!cL;0p)mpC`b98 zPnjKmV={CRmQZTZMHz*`_t}HEV;QG>=-oNlkG7cCOdet#4RFI-sVFzr?!J;P<Ppf& zidT5tWi=yf;6U6J8J<ng4q==pPk9civ>H$mOt^2?aWF&eHZZVbiY&C)HBo|pqzw6K zNB0rkVbwKdzS^Me>|NtU9!VePZ*p)et9q&V4IIaYwPNQ@hP*TE(VlXKW+{qCv25S@ zIGRE%*!bb-5b=2)Y5yh`I&CF!JI_w(QTK4q7%wu?HQj#m@ko>=+35a*r^`lwkV~Yd z-!})SJ~m=9Bu^0oO1V<ZjW&Jqu8#DF81h<T@)E`+H6DTe?m!&%19<=k|Ld3Ze;PP$ z32u)f)pSZxhyWsG({G~*jR4gt`*j8F4bY#=TFhU^m+j8w#75P5){v^Z&9r%cT26%l zQu?|35q@!7hZ%CiW|n4JQuuwZ+bTe2JP!Nuq5RO#jI!)`(O8_xo}Y=Zo5(TG&tI^b zzUtDC!?Ly?aC08_$o9PIFG6VEOJ+EWH2bn7l^Mneo)Z6_Rre_~2`oxm?l`9iUnsb} zp10~MIPD3Lr#j8hmfZOOaZDwkJYo(C{6lmE8Yb;?`<)dC*~fzFsiaD!i+gxIo<g<Z zY2+D4+Z_rdKxF-Vb?o&OE`F0Khd&)qO?5ifvIlPD!vrDw7_y^MKo|}yua*V}Qsmom ztQh!m)R{_fOF%nY07iCX43?|CV<pYy!%;fHlq%!-WoAc>n%(aW7JC`K<e1ZRM2u&7 z({~iS#oK*_WxJo<g1y63?T8SS={#NAOu-yJ+@76~tCEh=HGV=o+Ek1JjlrfA(2*}9 z`7>?yfG9)&aZ*-%mi<IOV6hs2Q{oMbs0B|?cdY!ySIQB=_@&0?KIU3SZ=vCAmRf6z z)nm0jqPC2IUga+fFS+^X%$~9m{vt@`JW8I4ysY$nqaKBpL~{_?r1K6<GH6##T`l43 z1fFkj!NtAD!dTpF^LA9SYPLv3h3_%m@^xDrg5mmn{EDboXoDT`!!?~TvAt^aXcTxj zQ1TCs8(Ws~LzU=e@|beWzLxcWdC`3EeyizHeL=pRsUi2X@xQ}2o{4uiCv9!K25XSy zhZ*G-c#j3#3>5u#_D3CQZle!Mm>zxo+fQZiL>pt5{zH%znmQ00;P`KH7n?DdaT2WS zbX-P3wrnHglyA}VL@%pEYUhnML894|ed@BrD?W_aVFw`lLI}KT`pNvWO!*2w9Sfu$ z0Ml>iT?uNVJRh#Aq-VSyFMH->-jrOPVGi!<2Yy*#d2g5!yw$*pY$UVwzY9=UcVOk^ zoe9nD@H2EA(~8e4!QUO9vCe;%UzRc-GIqKm=kqh=60fb)FriXw)furWCGUB{|BwC7 zi6~LBN>#c{5LVwDB_S+_9!(>M#8S5MLlo!(-B&u23<q@Mu2f7xcPm<zq#bnMEZasO z%AYlANX_CvcdZ&TcH&<phFOKjhVb!O6qi`!ZGQbFrcoC)MJ(3aZzjiBv(0;t6o1f0 zr(6~3cjIKGcD3&+v>BWDEA~=^PccU1%D3fc-t1Kx`Rd<^v0u4<k-E6=O4Vyk)EUc0 zP&r<;d}<SDT%?{y2UW^5l>LJUDEqM@EZ{@k*PY)^O@zrfW4UsF7YkgrEPbg2M*!6T zrE(4tr!}#}Na5V#Tckg+`5kD`l(~Ec?z2nTm?VGDFDSLUX~>xWSU)o|La)n^boGPC zFBcL;_^gb%K%v$uCcE^bWN^8aRcfX%bx)*lHmUK8H~-CQbeneQ2U8m=K|`caGvW}m zo65uWO~%aLs?m;nj+F>^A9YK)9&37DS9t@bGwa%Co~9J!OCEdLuUZt#95nGxl}+eO zG3<s}9W8U`-N6~ocBs%A;qW~jS@dY?kQDr47}yLH{}JT)SmTBF5=^S+7-`q%evYy4 z5V<X?<4cNNwyi_23evs3$7iEXoO)M6C4U~)y(#f6NnwIPY+5_yhxgsq#LHQchoe^f zQYKMc*r!&OipLy2ge4Z0xEC}=#Bp=YdPm02?yTWg#aKu`aXuM3#?JJrmL5Mc;OhB! zkZFW^cdL9aDl4&h{l?}cqMtt?Alz=xe19cly#ZtuSP6DYkzj#o$oBqK;curwd>7i5 zW1nVosR&R}<`1;NH%vc^6sAPudW*r1LJu@Xq)d?#j^T>Q{pS(S_jfeia~!5c6)F{E z&NXzNwsuSCjZz;===IK{>eiSHBpm-^UU0u`&oO*8LaKgwnc0IqA{wZ3ArkaMrwreg z*RY(tCF1T8`ETMzQx+20P3lex<6$=f&U;>Ce|c$>b7CRWNeL7DcREXA<U|w<Wd<DS zz=1n|H<jb#Fk~{uA2F=`q1;3Jef91g)xjGi{o=%i0}ycnv2cxhu~0S2Zas||xp=<Q z@sSXf`|juc=-&>6xkbWWgkBV#@Qm?_@WNl9Uz#j`OZ-*@Sy3<+EN^+3K3Z`e&nAHV zl~^L%^Vk!!Z9DM=zGX4l`W3zN%09VThPZ=(gWKiD(Cmd<wyU<jVcT>b8k+mek-U$; zZzj?Uk15DQXW3#SgJ~@xtn@Q$*h3@o%rYnM5(DXYK7!^=DHQLEmP@F@B$$Ww+;&sD z=CT1!92n#nlIxs@+3=J(<hP;&7!poB;9MWPF<r*%M`pj}Mlx03pV>4QT#qD9#VG$S zQ0px~4H!gzKstZ7PQ9vy$;{m#7L&6;oA2vCq5!a^0bvTPgttK!rTm7`%4>;BJOO^6 zAF19LrkJZxrQek1mgIx}){6O`<qE&K0_9vzyz^Nd$nFX7ZN|Z7`S%N>CwFZU&qK>h zRe`h46GP#P)qt&Jx+E4@&<;b&zW>uPyK)y6%+1YuZe>VEx)JnAf3#D;C?MP&6m}oJ z5elH8H8i25)_Yd2DT*1~Jesl-R({8n0pxKFUrM4NWM7<P`u!7-FmkY8$dL5dD9Kjw zsF}?7_0Ga<mUuOGSPVBeH(B3_EXr8&a#=$T2z)+67Qlr((dc%b{F<!Lv0!%w?LYZf zm;P4{q}1fGSd1U|2|p?#-)xn_L`uJQrzqP@Yr|{(CLf4XFW4K|sJMx98tDmVlYF4d zy70(U1|Z4qmmL9oAn39*QZ3i}YYG<K_AO89DhELTD>nVrNBgY~i>?=Yb8;P&F(?@n z#a&cU(6B+(a_!QY>$9nF+xifz%Tu+S9Zy#j3%@`$-^E#_C#<qt*HrB(j_O6fK`R}y z0IbrMDXyVyk-+2Vz+dtBJpTIY@$2hA5wwG#Mu<4(<D7ONJXlCappQ+TjbIZZE(71+ z(5V|VXa<?Ezzq8(30?yNmk*kL@q;`9DrHak!uY2-+$+|*r0^7)I{8-L#L`#(nXmhO z+wCvrJR~y(wxrml)%{1X)l94^ALd*b(?Za0D;+L*w(mk9<^t+db&j6*6v`;Acbx>Q zU{?{C!3%*;wJ~}X>b5HDhb5u@!EH4~E0@a4{FQ?fg2tx^X%Kd~vC!+Db{xL}yZtXj zqnJfAA$>li5zabySdc616gXJ`CIPUJ?(pG{Wc!cJdxe{aNbT@Me;J{h!#)Pu^fhUg z11)4Q%XS*^evD1TG2Dw)?0h_Cf&ZNfpQYDY$Kl)APSV&{;J=$s3lM%4e%;hR>)GWP z(A;!Jah<S`Cypwtk1j&nw84NYeh&`y_wwe{W@sGu{K`T-d=JxC*GK7|{2__>b_^)# zqpoy)A*yt2s`^*PS(K?fu;F8Hse!>lix)@fGOUdV#spTbqyb`cZb$&E*>g>3BNS2b zsDIMx``36hgW`F-v-sj@;o3PDT@LQ;#I|IwQ&aF}gF|L9BL&aCmnF+^hR`Qn;jgHF zo&tR57i&D>Pce%z@0~3OT<9h7H)hf<v+pGqsw&jki*6)0-fCYERNBhhEjPb>pd?Vj z4;-*}NGx5aLO7^z$Ova(X!qrrDIKNm6BT<X2SYXIiq=I0M@76fu>S*csd2gqm*Ev} zw?YlmTX~rs)lHZ~c10T|=>LzIQgZX=`CW;nJu5L3?TrI;qTl(xU+lb1bi~x$14f^= zm(h1AGJl!x|I=i@8^t2yDVs93pSbYrp}xz-UOy(_3Q56C6H?%G%FZwOp3mq%sYn*{ zcsHEzxkuW1%bMNV!_a9R72X)f&|N^D43g^UPMyEAm^D&2k!cwHoVZRr3)KZ}Qevk- zK`@;f8jxIi*oxBWl3VQl9%cb~WMFo1iF84ceC>tY=u^g~Y9vZZPIUUG?SSeTWEd8O z-e%&0Fk@a*&)YegR+8Q&fmk8h)y{Xd*{ha?>E|-!)O*<i>dsj|N~D1|2U<VPKmg*x zPP}ug4_19r+)*L;g=(HgUD7^K9sY+%eF{K9gavWY&eh=zx}AxUO7&Lz%mtq7NWhKt zK%0cvDWuJ@<A+;^PTTfw2d28w`A=SfA&!Ys%@Tc86M87Gy%fB3X|#mO5O2Z@VE9ei z`At~ged*vMV?k4oblc%7dc$Ldx@-<#?tSpb#1Ys(`};^WltiU`0<@i3rDo4=dP3LT zCIC<;C+obhbEo+80_#BkAo5qX11kq#eKivq9>8x3bfQ)Qy3s*{8E-$m!qMd9oNGZ+ zLBX^!Fh$2fPtYncAWGq2fC^Kh%?+lAJ=8BmA+Mjh<KE#wp$L4BJN!G#Pv|6j^t%5+ zOY`&%utQFZsdV6IG;(>|v+LbEXGkD*duiJ{K6em~^NT!ydsx~aYKWmab1*nS=u^ma zHAbQ~JKwY$h)}fd+`Cx&556BgXeX^jwx{lA!2*EQ3rXR<B@?JV^$NBrt5ku^v1RC3 z?dqYKfQn%`L0pCCG2%oiSBDW4PL7<M&<s<S4AWAeX?{Xl4@4Z7b;uCt7soU$n6y*z z5L94%BY)@EBKs4kBL0D*Xz_io`fHU1vD@>lMd9`kqEi0c?_~rMUoB&YQu&rd7uL+3 zz&L2f<eBk{MVO&4zD$mc#Ys}m+~VNwEd<;*XWNI31hl>Pc-Tg-T>k6Mv{bW5%G1lc zY3i=#soXeh`mm&t1fZnU5@r;-%{;{c(T3%BC0Y6HcSIh4<u0P!KB?&Iw=<!F?z%xe zOh9?-R1fqq#qY2Q=VAR<QiPyt>U@qRC}Ddfa6z;qT%uYMBLSkKQYYNbS8SuHRBl7O zjHu$ronYoV=M@qOJM`bhZajZ)Jk`v=qDUUgqXpE~4h4;<xOA7`t21eFjy!vIVLJW( z(&Vt*PQdBtWyZkn->kc9sz%uOSNOlhx*pv;^{}>gx&j?h-DFjV-|^97B~Rw(?KRQ8 z6MG<lITn=xOz#nQ<UN=c1xA?XC5<P*4ytr<9CM}I!r^|zi2BzzXha`C%^)yy(u{Qz z&R>M;F#NTwK%%wh=jTRA_31nWJwETK`!BaI);Yc$qF;)rAMp8%??EUe{Yl>}hD1Qf zMm<6UVSLt7Qi|eLi*E@)?<$@zMU0rE@ay%l&yLs`Vt!C>V-FmbX`?t8SI<jH?h3S+ zUr|R(Qb=^nqj<;o1y3b-P_x85`ewlemX&pC^G0BsSu?d95{;Q9hYlNN$OUB2gq}<H zLuiZ4#8~Iz3|{bLf~;Qc<FQ{}N(ZVDN6)ad{}>aH5J<DVvYh2FU&oUzLhBMcM@k%e zq<#9k)pqzurJ5;VTl?NAXYg~)_|T^);`5(GT_Qt8+J3GpIFcH+On;Z!Vw&wADeD(+ zV^-1H^NRc)G6{{17YIr|Gm7@O+De-@1)^J@D(izxLzGpq17Ui&^Y!dh=;}Nluj_v& zO9D$#D7=(7A|L;6E&$ca6+A)^8ho9d+05)gx{|y7OsU+Ke%N&<`%h6cszQ{i3MDB4 zu+wej99X&RbHWc2)p?yPcN{PL=)NGKm)}udQm}5*z<_;1o)bNUd%f-Z(5KcOo}A@r zCFLpRgnUcsWvBD0a*Plyd{r6Snu`KRB&22G3@4F=^aMpo(p%X|ZbAUn@_kd?BqxwE z9cL35N%*ymKtj%HIc6PUx1aEw_2_zXzEB?vQ(h+o<+T>K{GCE_4i`2oBL}xuS)iBJ zT4LCI_O^4?!*klKgmxukL;wVvW1sqPr(}la>Os!_eMGP~_kLux3f&=_a54MlnkvK; zE2e7Ax`TPdwCLm24o5H%0B{on<%9lzc~DgiNcx<jppXEr(wW&NODf2bGll`k^&@0h z4}c!gof{QAaGTF=25}h_Ui}z?Dp>NPV*3Gt&%nWmEtdGDi35c+JFo<mE15?@+X1Xe zVN1E_*8j*~y+jiy3!glejf%%c6!BZf7fAIPPm#zJW3x)Tm-w#n`+n*1)4jxq;NMd3 zEX4LII&WhayH3iIdBbF_+gnYEkg{4@^(H{^mH_Cn6|g%^Xe~pPEVnqe3<GkW0~;Se zZ?+9l0nq3v9sLcS`3C!69D`g>6~UrcY}Bt-Wl+Fxgh=Ql)EJ82leh*mxT`M>5sy@r z6ow3Ehy;G5Y3F~5iMpll^59ZaTr$IrZczT%ogz_oAy!O+j?7q1wOi_aLd19o66hT_ zXix7{OIa!qfNzim#A-j6u=Za0#KJ}0_!NF|VZ~wyj6!L=p2DV;sQ3vFX}}R?`oCN) z2`I(E{`T!)$heRWoJLl)27iY3Rct>b`Cuj(?X(M?Wu-2MVquN)<o*0EtarBl@#;I1 z-vyONR`OP-i;LSxm<!Al=kR_eyTk3&4D4!}ecCv{DI^s9pY|*GN4E<5hk#n6>0aSN z*ZD126(e<s7|Vl!WMR{RH6g(l$a+`s;J2&fB%mfea$m_C>V~3e2~(kSv?m6}nsX~& z=RdMJccgqDB9=8|cV;llO4}{LD+kd)+&v(JU%Np)Pm$>y){J+=S>KuKheeAD(-eig zZ<yT<@4na@5YTqBvKsT;Qv`oFU0mww%sT%MU_03{P0fhA$em~u?xos|v&RP_;p|3( z)1RJ-NImh7%*3?4EP+-1iGdT&S%z{antR!-6!y-c%oet!-y95_K#;8(3<zAGHbHT7 z3q0D)5X#LH_w*b6XK%mZ#B&vueDynyDHVW|-SOlXD4>|FNM`=DSYfO^L~(9T`a&RK z$=uqw3i=lvpjHOy+=(pD)?VZ@y4`KuiolB^n+Vr7k;;z2xm^8cx0GGI>1lndrv57V z`6|h~ta$h8Zxy~M0DBcEAQXYh$@&pPM6m4C#SiX=f<<LR7wG?oh!Yjjtp|vbEKXq| zxqp8PGkiW@?<rKIHwYAWqn*>fzQv0h-jHOP2GzQeF=9+82tFQzUbQv<1MAOje64FL z%)s6AMOz<>3wDDfl`RGie_<*7QlT>aAjIB&+|Yxw(`fuP!Eb%$dd7%3xzPTXn@b~& zceXdP-kdXUY!`eq;ypegMx`P*b}V5M!%SxVwd4xCyMCQGNicPT5dx+SI&y*KK}l-> zWVt^TsGfT_91u2uC$d4@zi9{5Lu6Fw)-^*veT|VYC~~!DAm)6RbMfMQTu61&s9MCL zbn9tdk>V>pwV`t!V7m43Qu2MHAO`;O357p_f&JBxGuU;$Le=g3_4jvsK6i5OFAO8H z{7=fyxNM?JYLBgSJ3jdYhMIu}D*+66JWHSjwj`xG!?NCQK_wl!1GI^ZXQZd{FtKtR zKg=-UebY};cH3o1+||YXtZc-?P0?^!VFAhs?DVR5&`#w*?8N5)%mwF%jViwBT6O<J zOcYv^Y%j?Xp_WyhN0{PO-$wfJgTVt9Ma6@B%ON@7y6VjbaM(X?CtN+e6LP)-qFZhZ zQyTheUdG6k+-s@AeJrUMg1Y&-{gt&f!<_Vma#w(ed8tjO-~|BP)e@zrg&2;av*o~z zvCKR4tdO0RSQqGarAHA!XW~M|$d+zJ)BDcqyowfgI|&P!7UPJR@F33rzB)oCTzPh< zQfWsr`Kf8xs3;ljRxPr_TqO8fWS6fMy=`-O+53SyODB_%nwYH}W7;3d!(8u%b*lY& zO#ZRwO7)Ny7jY@N=VQC$^T^zKbSApB^AEm7t4c0?oNbtL-+lfRyT&3(-<973ocJU@ zblDw9Y|oU^mug3w^Ut+Vg2&l|q3Xh@dQc90I+;e&ctE#yy^!*Cqx>EB-D9B}H2!H7 zt_7g}@N6w*>WH2O-kV+ml6YYiv*zro{q<;qhK`Gn<pp_TSmJI)=K-cFN2Zk%kLQ)& zev+a4_AQrtho{F?R(n^Kv^V44Vwp4X%9ARfi<Ziygq?c$^MZHuyI-KdmdeXW1-w@b zXELEeXMfxh<m8Zz|1b_aD$>lf({8c;fHr=+kB2j*x_#t##n`X-0!DXsR!WKQze)V| z9AMumK#frV1Sanoal_G0Q2t*oz`>3UK4P9#myzV^fjc=>fXodYeN&OpvGuH>3Qe&A z_c>mX!oOhdy0XZ%U%MQ>OHm}i?<r5NSbz)l%kY=YW$Em@*B=&61wQ!PFbUK;p#LA5 zzB(+*?|J)Kx)((0MnF`g8({(IMi3+yQ0bDc-K9n8knRQvKXf-pcZhU%H|*}q_jg_I zAJ6|YbDlZpJ~Q{ZXQ*yNK>1ye+#q-FUWBmuZV4CkCp{)gUpoDdk2cxSdV~EkEo&dS zib+lQtf?1)%B;~d@xFax6CjP;mlIU|cuJ<_g(QBD$CYUx*fJ9p^~`7X0%Q394X!;p z@+bGXC({Rgk%WZzJ_qVYxxt!hOzKW4JwKHRc3y5Mkg|ub{PPs~OOY0ilMJb<<V|F1 z&?#%x4yCv-Li7xBcV{E{_iO%rPx_o|drH&&%(3xyvC4+f`-6SQ36%Tl*qchh6pqia zJolrOU*Y}nd%NB4hohRLwohC?)9UbhevYg(?k#N->=ki+$dw{#`H$XGovz3$vp5UW zhe2-ChK!+9Dsl=xfo2fXXa`2_fcX1JfeM~`(fx`znCRZ<{p|Ng2Rv9`14Fy*E{}Da z8@2_3{je1Q53A(%aBvKUC`htB`Q|7tZB{44AGUIu6ttU91FAtyXE8NCge<qyqfbI> z4jZj=*j{l<vo8}ZZ<LX(;xi>W_ROomscpHDp8<&VC(_(5zZZ!iT5pKEYK=F(+y8lH zU?#r$O9uNY3~+mZh|P*Kw(y^REJz_a{^$R-0n5<7<!AYFc%U<35@zFZKp#ihtu7rx z1S;$S6q5Yl+rXr&<mdXqEYKSGxKm?86!F4`F;=QgXrD|VD}UI_%R332aNyu1s@|`7 z6ISZD4O_^v6)SG2bT+J*3yy?>d-qb>!ZXh)DtfU~mwy;*EWM8#<>X+otACj7OsQio zebEq<%GEwh@kHAGu4t>pd{({L%!xgrPG^;oPSn{-II;z0KL4yBz9d15&g+u;^+&H{ z$qz1cg%>|6M*FW;3$`d9Tb5K1I(KPkluN{mIR^)33=p&z^kfkcg5BPHDNG58K2C~d zkl@vCU}uieS>o%a)Lu}jl0QVVK84HF%{Y{vRN!N{y>$x0h1G9t3Zp1Uj{InlEq%5| zW;VOcm$PQDYq7VV4Sv{I?~N66;<#C0s66<g!Wd^bMQdCiR^Gfe(7Jyco|7D#p=tN* ziwzP`_%Db8NAerhfee5BQ=AfHazc7nnrd%Fy!_*3shHZIcpJ1q_iJ)9|JK3s>tlBO zy2y0`?>F^(EuECM4hKsJ#zLNK`F8Dg#N2MA{MB2f3rw*kW^oylmPFjlIglKq=HQ|0 zTQv5aO8Mr4Mn-okZT6p}S>D1~uIxB($*m1d`|h4wmp6?|iq_?2p_Xz*s;~QZ9j-{z zMgHSh{o6STjbuB|jrGs!r3$Fk)z-qtjjgh(wA246in>s$G@8u8E;_`qve5@fyS)88 zvQcJ~2C}r;-~Y7Dk3mk^+FY2IhdioxL7xz!qDT)LNLieS4PG9CBg<lmulZaSPVglH z?y`BR$!^$9%y>)q>6|N$$VOm}V;g60BQJ|bqh#pl@EU=eia&xU=CdZ~nL!D1?>z6i zDMe8q{s7KH`bIGA`q#1A2kMx<6%xMwk}4L9g=kPq!m;ndKw$0%82$;C45k2{DuZU; zEks+DOm;<RowwMdE)Ze{bs53qeyIHVRaUs9ebdoa>ilU&s#MgK*I)rK<tsD!SUI?u zlzFuiH?c!{^%lKSUG2ej<_E@Xal`_mBtvhdI~|cRb6O}DMN1V0LuFj?WuoXIy!g#D zyB|*gA*`GCT!>y>%091gS@l-xt)teJ5W@Dr4MugWOqaKX#ov>c7{F*%vyPjennJ(D zqF`#}`r9-A$;KAnH>;d3rAme39zU>z;Eql;OmvTTex_VaF+~ym*CWLGPhD$_@-mYv z+hii9(Vn60<6cX{Ek}A+-k0UNeP*2kr}0t!BJ>cgm5pg0eAX9bRnXk&{izGAs{<FI z{8*pX^-`ek&G3P5;p-AChz%W-sP+{X@!Kc7ngs|Zl%g1!q6ASQbp#eQTA>KGqj&cr z{f*{+>CBGw`BZa{?Ww;8j2OI(6n@;kzb-TD+hzP9thg2VP!7k{`6SZ2m`XGimZ-do z(91||)vKP(jajV{>@(hpi46i+SXFNC&lFx%Q6LgHv1ktmQhvr+%kr=~(lWXPl9C9# z0r1*mR}!I*TX`4fcZs?7XuxcVH{p))8$w@525(F-#qqoJVswI;5s#r&4>2Q>QKa&b zBX_wXWs!BQPw&ttKYU2&+_A~ndFD17z^fAZ<J+@o(q(?|RIvgpu14!`Tx215w(YNg zmUUj+5tX3pmxKDB#B?jq3z09{b+fF3MJoSO#>XxC?{%Fl1J5GDr4DEmhUE~Nmogha zV&Nh89<|{_`hkv~d`l(<tn5f(+M;ZK{)j9GA}X^zNjM?JBRM3niLr}NV=*{R!9=}D z*?55R-|w4;!7;xj3RgE%fJKyyya}C3IVE@yH<zvFF?=9k*XG}&OQ)zb6iiZCdfr_m zPkGBEj;SxyJ_s87*5FWERxB}ZDD#MVmMF)Pu%W*hVzQa4qVX#KlY>xSb$8@*(i}W0 zO<+Sl1!<<~z6o&nPBo2r;3;jq{@?C)c$`}qDS!w*n%2wqxlEus5clu8uU+4Wc`M?x zNBa0{L0yZuDSAdWQpP8K17$8gV>5pPBg>!d>==-xSf5U6igrQhrQ%JpKtF+RpHgQ{ zVbvWIlM0uap)&BbLOB|zKiWTcuLr&|kt9F+v-;`l3%$W?TFdYB^qb>L{CK~BNo-BM z4Pt!YtXRR+*nsu}J2;lm7xMKSqvtmm5hf=UPDDIH_6ziFuJlq`3KL@7)m#1<Mwue+ zywr2t;YHb|upI8fm@DfC{fYg>(!984tL9;zeIy*q?Jk|B*#o2HbdN-5kbfJyOzD&5 zS<G%FPXD{6+MWEGGIQGyap1*ANR}NL)gGbopOG3@U@od2plZTy7tM4$kbM3Np-N(O zd&vx!x3avPA-9MGyj#1P-FH;%Eyk`(sp$6tQ#sq+b5yw31@}M}5?PT8yL3a2A*$Je zB6Mc%^8WK83orEh3~FN<6|STNBVVZ#e<qAs%q9OU{T(sZ_|c$0gcLDxBNk!Os+<UH zZDD#b(~f-oI*iu`8ZicVWJ32y+1!%i@Ij_S#<4<&3AF9ZkB{5`=?8i|N(VhuXZt>k zsnj3;L(@-x_w4)knEdPCuVDn(4K%zu@K{K?!QrqKj5f58d@3UL*hl1QzG}U`b4v65 zeZV{ggcU<N<$f#RW05|f$8X=cu_Xy9C;X#h?)LqogNccf{n)c-KtvNjH+Gz$Eh$kI zsP{CP9|TmBYI1Yx{XoP+$ModZv*udXIcOdLr;TaLlXefuAJ+?avz3dAVxMIEkoE@B z>Aq!kuZsG<A}1BDb0ow>`aNhF$*RUA>cmfGZ7Is#WRmXa%*x6mocMk&l0!c_6uklT zhq#zt15?{{MnG2A#iJOO%YXA2V>6)8KgR5MW#D9Eh12*hgOoS@Vj_O7@Lxv9{eg20 zhfVc2^M*F2tY3Lv20i&9h;i^yQ)A+CUHZQz@(T4Q!sH5vgd>3oPY0Fq4E!O_4C6Y1 zr!VD=zM%T4_(Vx^;`TrN#a@>$z~;HKhG1lpr!+rD*x?RP#+>4F-HN<>StJ^mZ4|T< zz})_jEIFYN63c(!?uh0cY1%RVBV0<%$qt?hQ?Qg?T)EtT*SfE{+>g2a!I{7sb*8q> z0I1qfSs&fgc;`jTL6}{#!!N%>q{BrEu%QN^>`Q={9Yf3KAUa*z*SHO;cFV$o3rV}q z^D&<vim*~*qncPcKGGe0bWpthU@?<`{<`JmlII~c-QPC)K+C9H18O;;<$#%n#iXhg zUspHBu^*l^+qvx9RaugHIa7=&I6jje94Ywkx+^~&3ig7Krh$~va5oY_ugd?-va-j- zQ_2{_Zni$isjB}!T_UEJSR>3}pqVNK1Mp8`b0}yat9p>!*kjLZSLy-9M{8op_(d(T z`;CdJQ*2IXt~KbH-iUs%zk62g`WgqITe^Olv3RFO^qeTA>;B#Izrz&2tu{aF|6Y}^ z_9#=_>VvvaOj+dNKo<3lrCar=!NPIs=X!QBOLG)z?}rsM2vE#(N-AWy?TQtpAm4Vd z`!pU!4t0x)>MhF`I*8+3z9exk@FzkN_v1wR3SHU;5#lM<06!@qg2^QzhQv4Pi#aJE z@>iw=SXt=Hl|m8B7^BDz;3yEC-(hm&s15^tcWD#D|9#DDl+ft{a8~Jd=hRu%BdL;6 zs%YP&uN%TTK4Z<=nNlkG(*Y$srBf1xA)NZE$`l)?!Iv_Nd(nyhEQ))N&3x+~(BI$} zB1GUhxW<$5c1hx6kQ({b4}hZkyqf80P79}33ubPq_EeVdZV&n~=iFx#X*^Rz=;3~p zk!`Jn^yi~pA+`=ZX~CtmCBn(F|6SE^p6-i|3j{P~{}_}~J0u+Uu`|t0D2)I4tBIoQ zJh3oMZRTwE-ES%HK;wM5XInb%Z{E|M;EFtFA>3l?T$z>0`i%rcp2(!W)_$jKUDHb2 zOS3b!-<UP)9y*h=FSf;w-%;3Lp_LYlU+|cRYnIEmE)w3?iDS$6@=Qb8k{0>%v$-p| zS3VlZ_Hc}zehJfKgYh47;OWLlv5!C6b_Aac&en(IT?PN1-r*163;tkNzZw^DsXG)d z@0||Z_44LM-i>TB_-o}vyD`>a2yjF=;H~vW@=Qtwk{lI-4)58&k>LeV)svx^&WvFx z^OQn=_HK6nywX@hIl^ef>rg6-Q<I#Sg5l?mWSYVc67Tezw`k_nYaN!=*vQD<c%d0t z;v5}rM9uB#_rd34YvRI3lvqFyB?j=N$BS^m=bS_tu%ZbCG<5?vGNNZf@y$)h7qT@v zMFFcrqZy`B^2c6~*>NP}dLjCZv$@ss-$h^w3#AVsLTsfJ<?@N3OuzNCWqoHntWa=v zDg5@8Fzsl-?@N_m=`knt($sE&JD$qEe{B_CCar*-1tVv>3El)unewq9d#@P7FqqY| z&+%olYFz$S+cgRDdE#g+-9MA?-L~W%yYEe9oFWE^pZUgN?ahxqsc73uiOTis7;?h? zY46MZ!Sr&Ue3tw7e;uag<99aA&+!6z@|Sd<S-Hwa^IgDzSYJk?W>QlBj6|AP*%9<0 z>wiQQU0hT7PrmaG%KvNL6mk2swqzkpA$EFq?zv)$#<P8c#v56F0ErUM1No9)BWjB# zRl~4p%gMQ<LcYt7pZx*uG#I*9{#v{szRs0c0F!nMQ>=a>_*3VeQ)bJ?hi~{{y4i`? z{WV7*moQTJVa*UOR^|m#;cg_OY`MP1WD2oL^UE3baYk%}r;LEV4gGXxX;<-1H$0$s z3<^?{?qdU>xT){IO_{P0bW=dbsnOk6eBWLPy)W`Zl~|Jlh@>B|)tG<I8wnhhyKmu$ zEm0K~wxxwEb#X0D;`O0Nu;KR!tj@R;QPpQEXI;--X8!GrD}CqSFQUh`Tz-3k*y3rL z^VZj47pFx>7<QE3HMjHKm`a4PZRfYjEBZR<<uoI`ZOu;NO>qz=OLk<QCQspf&tIk$ zishi7z*JndvCv<L$97Hjr}@LRQjQXZd~GXya+!%=u#M)cm1BKR$;uYyNR9;WsKc&6 z^d0rD)`u<;^nYOgY5J*0Xjr38oF*E;ABx$2ohjita(>d~$KeFs#uTYM2You@dem@e zsyv$je{2898sGl4VC=iCBhvQ3>!N86c~VC^A8gtD>F=92x6`B0(?(n3RvN%n7CQ%b zT8AO-7&|&;Nwox|GziIG!oKX*%b0q>d<Yh3W&{SLxlV_-C8_X~$RFrBu~(kEztL4m z>7U!PQhmN(%`Jkad#OQkYx0a+^30y@?>&}K%k`wyuF<^brZwQkRT3ZK{4j0lY=<VH z`-c{g$MvDzwk6V`IqJ%=_kb?H_prDvxHg+kVDyjc7w)y&5co*(!4#?W+Q?7Ted*eb zZrN1ocHgMjqTj&oE@3pwTD?=pRTiP19pCshzwL&TA6TV5UO<pycqiH~E3AmCXtjIA z5|TDq<o@s<D8S(Wn)d?JRuoHM(G&ou3y!lt*%oj5*B<wc)NS*6^e0w34gB=CsFw_P zTb7kJgKGM2J)s@Hc(Gm<@=A`u&zK^Q{zY2cXY}sMHm!0b#l*=?pW&InF4XDfYo1ZI zp}}J{Yq2!>xqrZDyQ6~+U12yw-<5q|MkpGykp0Bi-=X}Q;4BQ|)th@=%@)twq^GpO zsP0iE9@;FHVofyqYDru@cD)sh{Gc)4?BU~4kGHUju?Awo3m{b_z_9*Kp?as4b7M7M z9U!OjHOK1je|G%^ukZU;S(AO^xmp!I=DiDx$J>q3$dP*j1!7zp#eu|g`^YNss<_6| z0k{sbq{PSji?`6R89Z}8H9do5dioN%d8ey5p;EKVgAt0an0ye-o=&7gRHKhU+WPZi z#xeUleC`U<KpL-Dh1mNgGsoh8_XUDI0Pu`Y0(B8cC3a4?bmgyMejXXft5DVohkn^0 zdc)}RRJJffu2|{Arm~i`1BM22x6lb`3|<*&yVQuQsC=3WI~H*jcBgBk<qza=KK*me z<9bG|LqKSln_UX0C0V?Wn}c6t*3P^;@0~*arI1UvnBc=J$=#)x*MjdZV+Ru1T+YU` ze6jR<6{n7q5-FKZ<jXo<)|t}~1!H=A5bgQ}^BOj}H9h%rQFwJAj3Z9aC)nEkfdzJw zZ`v_@eD}fuHoI)iB?!E(gEO7Hsy`X%sz2H5N=mI?*dqQA@}~KLlprq9vrhqz`Il-L zBL01%y++t#Wj@t;8&}f5_*EaT_Z{{SQKmHP!bo;9#EbL7xQbGoq=i_VWHF*_mcDk# zs)OA&m1ANx{%nw{XYNqP2d7AedBQgU#B#E4z8`>lN^Z%b@EB-;{xnAb6Kqpr#z5G2 zm@FpW`GvaJ(x%|5e(ItA@P0WZufJ4Y+ldaorojt4eX)e<FqPIr*Isc?904{?I}M4g z!Rt|eY`^<pg4Smfwl^J=C{fskCF)h9keuao%{C+ORI~P|<(c92k_}18Q%g;*5y%Np zV?(y9N@AQ^o11G1ycSAk_m092XiR6MhDx9{89-(G;vMcyCO}ZoiS4(q0Ox}|5z{+- zKOjk4kIi0A=1y>I1Ixvh^)R7o6q>OB#!mpQd&kMW>kg!^3l5V;E7CtVtXrL(?cbm% zCH<S0&?<9&lj1+|ruq?@T*IrI=u5c8&6G?3aUD*Ph+t43`eu1$kr7QK<}F+&nH>yg zQl@W7?V2^_)8zh86|oMj?QYe+{Jfa09gCwmEhWk>(}F{q8kqQke2fS1qs9lMbrZ2Y z2A(MmfIcxN$QgGZsr)pjX^m_;oP+DdeyI?f9>A8kuR#aR!cG*nKKD2hXV8x$xkhli zHa6F9P{-&vH(XfG+7xNV!&yaFMZ6~Zf$I*_Y9W*Oo@)5swc|Ly!|V6xQy0mn?$0L! z;^kZrc2{K=rEZ&7q7^+{8Tx%#Pj~_5FwmS!uxZujj$}6+WgNv(Gc0*OGj9y7Du233 zFW&T-1>5GUohg3Pu1PSez+{YX6|hP+Fo2^Tm+)jm4@SJ%P#j|a5ggAb8oJ$fWxBf4 zVGEw(T2{#w+6lEgBl;2<Cb}>}@RVsdB_P;<7FTABLV9=_Fj{G%z6#^O{3~~Kfq7!{ zQCM~ME;sbQ1C?N|B+H*~ejf22I$bG-IyRvquR85*5%Qz0|BJWj{y;6^4rUHcUsQ${ zXjJlsu>&bTvV1AWTA0z5yp8&b4T+%Gt`=cbn;(nU>UkT8UH8eIKkv~0B#j2gB)zV< zGroW_K8G!yO$XaQnj-oeNU@h&&H!Y>wMWHBjT#co5KUkAO;NWyVUKl{K)4oQ8dfwt z+mATQ-9DMh^ix*jRR?S+>U!uTz3;pvkke>@fs(|a&-Gwcs(t8W_cM;$kRXDhuMa`} zXh679!*$^?O~S-m9OvRQB1BM8Jn8fi^6cPg=Y(V<NO1rVX(L@?W`1=Ju(EY)p_8kG zvW~DW=@_UiewuaLkBFAl9^%fp3uKY0fit>G!G7gzMIWnRPB?C6Hh80OWF7PV7VH=N ziA1{#7)23aVC{!!EH`%ERw#ewMe-7<wB9+Lwuon_i2058+V=dt!%$paTPaBKg>8$V zvDamX7Qp7l_BjfV0fN<fWcX5H<A(VXlQR>6)3!VHA$_iK-GiZ7TxoLlxv|)pUwDR3 zL(ht}5Mt_Bx+dJR^^S3+;uM>oLbj*aj1GCVor<8b#hNNx`n<3ufeXF%%0rbd#y0$^ z;o#w9w$c65KKlif&SX=goPiTe=xL4-5_<<cXR3I2{q3x|2E8f}+wgIa=%uJ)NhkJJ zhhfXFE$Gg_N7*=neX;C81;#K&h`nMRE7*_0Wj*o3Iq!#{k+u`AH27-jk0(@v;Ju{t zuqU<*JPEam3Y1%`fTAt>eYaY)#b0KIsT|JeH#hhNKrBB1Sz|H&9`AcJ+Jz7oBh-|t z|IZ?g?^A)rxVbHH(A&DD#QIy?h+b|wTS>$>8j1U%6KW&R&DCk^hnHio;FnEQqL&Zz zfSWmF(vQxjH!qDbxydx(0oe;Msfibj$WCKd->gede+IO6kHs+K_<??>c_R%MW|Oq{ zdWoNCAYRC9@u>L?$=8d&?JtW^X||3{Qr73QbkhSa{kR%K5Q*FFK*egFRb3fvCy8^8 z%+4`djdIDNu#_5BvRr~pja3)&`$9`gM0`eX%ANll;>EC-D6sb)i!J3fIW{^#5a0~< z<hob^0w#V5@)%7A?c$z(;}m(77|<bxLF=K@a^1A_QK<5s)VubV(eJVR=SO<XY#Krc zmcSGjQ>89p>ag~{DpQnNn&-wVo*upr<WpfakxBd0yqppEQ7^4{$+0!nA%r|i$<jc; zt?%<1+@gdtR=hqWwhkytOaW?6+>ZGgGfNZ7t5F%GrK{@AL&#;<?kWjP(tk##pCT73 z&spi~jz<{P?(l5CYbVnsB}Pgx8h~kdhKMq7u1ia2Z*-q?ikQG}2t6Op(HK>^lpn~Z zhDWSx1sidQSXA%0i%|xhjR7A@_>5NMQnt++AZ{@8?!UV>KsVO>H7GtXZ1w_MRo1h{ z?x$UB<4I^)QfVR1e&|mks6%MY!A<>0g_Jvb@i?@Opht#MrQrxAgaelOXOKQ!7c1a= zr0aXkFBMA3_`v-KocR%3vo8%SAleEF<lB}O1$O7h>c@$sMK;^>v@ZQlY<g^#fxMs> zqr{W%l3&~y<BVm1p$BswgV7=yWxD%Fe5`y?+PgT_S*~0hFLo)!7b3+FMd!7o#Dwa} z>;YM0k9(h3$=;OgBmeZvKwL!ur|<7&OQ-f~<h1dOd{3Euk0%-XSTm8>X%8Kl<_-sI z6M<sCT|G=b$+DAAviGWPgsW3H+1kajLyi5OJZX-{WxDuCW7;gn9FSs2^bP~>qL+-9 zDwYCUSI_)R5Xb$`%3>U84|SF!1aq;BaOIATv+bh>z4?>xgw;;w4fOLt4ql@gs{^)C zWv^;H_b=;&;jN9C2a%T<`)zarez#Z!+?nmR%MTJ_-ncxK#~<t#J*Y5!96Ppb9pBBz zGq9TT`Yi;cG{;?y{J57^@z{9+)wv$+z$;6UGh%+Rp{9c%A+$Lko<fU}XVK#@F>ef> z%!NWosPLx<-tR=?PE{#t8$ziAa`&QLy7*>-U7<+PMNDz$$*2e8=2Ikh&8bMK$}C-d z)#bVHO@mmQgy4NSMak0w-iP@(Dwx`{NVAMkRQmmIWI>%ppV~Ea-@i_P^CkwWS}T!9 z-&^k9IEYeW2bmqh4WtPXKiqDDm2e04z-@fuJ4SPEKr9X^A8398NZQa4|11gglMbo9 zyzKEr3l7F06i|QyD>M&7Yco#HMN3Y25q-_`h@4c7O?I`%58q#J(a*WBaN)DbB4_6x z7QPDXm#}eSoyl6pFpB+nAN4@}7_Q3eBgU={##^cJ8m}fXE9NW*y+EFAIrVtWt2V6X zL45fUb*45Fe=mie*+TIM-j85EO(4vrOiO=4@t}nRWTn4j0&0G*{Qye#_U1*kxMX)e zH}#8JMZcksD)?P&WmAdDE-niaGb|q8%Ac&?&P7usk7T$rSnFD$TNrf)OY2tm0Vr&{ z<Br#DhDDcPQsgpFs;jH*(!=S1Hghtr=tH{{sT*o{SUpou>cx;%>e3&Z0*b%E1K6aS zEJ4n|o&|Hj#8f`BKB8#?K28K70i)fXX*2weDzDepO!}?&X@Dy`2P4uLlaCNO6!g5^ zp-*e3<um0Ga;{CI^``r!rou7>oo65JVWc}Q3BYhuI(AkXRd3o=tblqdbu+}BCBWYZ z(fs#5MeN>unIq6Yiz4<5UpiLNT3h^+0D^&?F({8PcHx(3?+k<6@1(t;of5R<UOM7X z4z%-}bCds$+l5+63Iq|WB`D@<M1VdDj|B3I&?n_pJ@GQ2u3dH#K6*ISsJ+#HrBDZ7 zVdUgu>!=c#@7CqphL>r=P6QaxEv7PVuv+XQL;r@blkX;7)M)pdG<tAvAjgF-fUlTs z9H5;eBy7zHiA83%0YPt=q50@FHNdT6C3)=a^OfyBI>C2;(N1>HN@x#k03JdH`JSdp z$}+Lm^2zC0%xe^5{iP$ys41%Up!&Y{zVf=f?eERUCnOoA_Dg7kthJkG66pM8O;dyL ziUJFA=WVtIoZGRqaQGo!5R*ORFdlkxZc*+B07WAarc-eF!<5P6ssGOffKLFAG(?Il z(8*bM?p=U9XmyjtvzW9|Oi+Ko$%+&a<~2}H5`OqVLC-k(x9(wQbfwp$t|8<fpZ4-q zi|2hGs;uiq6a5<haP0Ewk&%Xg`x>^9@9MwP^-?ZKSB&-Wa<T6H#~n}Fvq>}QGGjuZ zSpZinIcg3aOIbLY?!ge+34ephL=Ii0X&>uX_`<gF<E~t}mOqS1s^BTcdf6w^31<o! z;TeFU(z^R%VHgMgBT^V?AJu@wL{t6Gn(F&+D(Z`d?rWj&(@8}S*Sgc!r=!EjJseDZ zdGGZKbfJMQ55-s+m1~@bt-8lm;FC48E2UxqE55?u_&f27CuVuh3J2)Dhv2LaCGBCI zT><cZ0RV{BJ~`f#8dh-oqJ!2)1MKgL7{`)8;k&=u*%#tpScj&rPrL79GWO9kJ`^g} zD)vphAFX{`k0#HQooy$n7WPd0A%fIOF*Yn^F}Bx9Vd&YUY>Oius%E24JrYd&4W^aR zOT~nl`Ppu`m#RgHl}lVIxWPJ&g~Urx-0AoL<a^eglT|2Iz+WZfpUQ8uW9}wkA)U0) zmk-kE<Ls4&?#*3gj`ir=`fJ7qiiw5~gA!a^hhh)v&DW+dt)7%rgxGld_^5UYbwtxN z4i~er?^eY>Zw|F##E!W8$a_iNbhc9ZPcv7unG??CDu4t+N}Ox}jf<GL*nB^tXAVdr zQc%BCFfb^JKtv4pccqBDcv&7rywz9J-HP!4EM6;AsX`oMb3nZG)A2uQ_ipaiJ(gjN zOXvNgxsLa(*b-lT;z!(8xft)ZJ-0{ELVU(8Kd2y=+e1d8(<iO8)dB)uS1$%`PrHww z#{fSXe#Vg#wwFJ!GBX>U5YmE*)S_ev7_`yY*OuGoiHdyhy5QZw&!NZ37a-0yONT<K zSLA_T5=(m(cc+B;RsF>}122+nlvNyP9`7HnwFRg}+|woaWvGbfa;PCUb`4-Y)K%l? z#|=AJ_Inn(gpAoDX?r)Sy^Q;w(!|yiO>96DtGzOyvaIo-9<H33M7^VZn^6LSHlwlJ zx!y(GX*h#8wUX(Gf>%IUA2LM!D-Vyw&^A^8ENfT?S{D^$dY_Vbcw_zjOqTEFeOL4X zu0`^Rx4*Wtz^W3DG4D4rFh6;aE60Dm!VzpUOZdgABp$GvMCX-ssYqdlJB$!hA7xQL z3h81y+;X3P_fG4}H&osg4eE8Ji>1}DtE>;bH^qsR6Wzo%aqOcE9mL1?_j@Y;jwhHu zT!((m;4ft^{a9rbI`-@&c+48oDS!VQyrL1c<MR6Fw&fc}xt%@vJ3TTX(9a-{B5-y# zw=Om3^H}Vqp8H@W@`@k&^e>lBxFaG5#PF2RT$^!NhofGdbgP_eNr=^|f%ToA`1Sfy z6xvef$D>5Rtq+ytok+nW*|$RX$zpk~2Ve+Z@LzQF5_VrimE#O5a<7Q)r8u+^G5X1r zaaKwXs^1QB>@OSBCB<o8_@&$>tAc1T;xAZNJ&_TZn)WrjWO!r<rIy!J&x7#32ZzON zUtllZz*#S)y0IvJfuMH3*gei2`M8l)K_3;f#CVIS5Gj*cT~tqG_&j9{-b1dxN5GX8 z5^To(hl_ScFQvA;h1bWBj2+qcQWzbW8@L*|Jns1U$<8`oIGE~7>M}>%a%`dEjY7u| z^ikl>-`YYiaf)*y=TN1v$tcOWrMJRX9RgCK;qwo4|Gh=7GCpZ(WVe_TIbNVADp&(Q z>O*VKS*3(HxSc7d-yM%?&<Bz_6g!@wW}$)zHenC|`ML@pKA(`<PXYbUv9=H$A<O5j zrNl4PIbtSiM$5J|uE?F^sJ<^Qj{J^LRI=et?`Ur|<gMghEs_0!?H0>;S>C}KWxh2L z_UU>qi!FO%=*536scUefZ()V5@Jq_oq=pg6kJ~rze_*JbWEIdM?!?C*hU8LG$!pNY zIHaX0OhOL~3<MpG*BY;)>KNurenhUAXwi{E10eHH&_NyeEM~2c^eLl6bM6<^>Q^9k zL^>Z=I`4?!H<?*$XOlm>lUejXT<<`TGM)Bu^l{Y>PhHw-(vk#%;Xb`4i`N3RW$+=f z!Tfd1XfaGp`uFs08=mNztJe<=4X6B+Y3y;=%h+vEI1`NeBAA*BU+jhw`ECenLiJPL zeLiUZZILCKn$?ZNIn~u+^9cW}yM?iZVm(C0l@X1rP^%LneB7v@ZWZ_$JX@fJ3~g$C zHrO*WB&Mv^hfj0@65k&U>JQe`Cwrnjiot@TMcYE!Nd@S9U+3OW7KXH3R6uzB=bl1y z<g5W8&oud##W)GI-XjsP7QSZn%}K1K^|#jNJYFW9?IbbKFofptKoGF`nYBm=)l@(| zOj3!R&9RtF;&8>={k;-edc|@)=T{LKVyoh_>xD8>ft9ycRAYcRu^&30K1}>tm(mmv zcHfh;dHEWX=@|ZO21G>&gE|udK=;XMsJb$z3D5A@>CnKA*EG~@kS68>6NCmd85i>6 z6#>s=P)yePEN2lp?+Zn``@7?{gIxIUKYG>Dg!!s^rjx(3O-yqIU$d0nLn00znc(}n zaN*O?G<YuMK^jy|y2hzoLR{oXK=>u{Rpk}j>MbOZq6`GAbXK5v4_Vv_#GJ8NP6R_) z`$C);o@V#UyqTLa@@Fr_Ysxav$WrgjN;2@gyyb=w6t<pk$~9*Y8@%3ZyqASWIt(v! zWieGqcKkwAg-PKRKgcL5mglwVtRRY}HB+fvzu>-P=gDMr)eCW+M!)=ZcRY_C%z4~O zzJ57KsK6Y1_0&p|600aYJv4|%5F^+Ns*fA$ZWB}uO!}L=RitryFu-x6B^uzSJl(LA z@*f;+^O`{#mQMZlvi0<>5$xSTdfy2QW4X?art}Ob^ok0QBi4B+2Qqv8g3{oziIxf` zh4Et|mzjC+t`o*_?TxPQYjX_&q=J3aMZ<{fs_2Cf!62Z%pHTCcG~PZ3lWza(hy#q8 z&)cD3W0frRfvkb5IL`wQ7-*ftv>T`F&E_4>m}q4Ss-<ztCQW2}JWrT)r&>xC5A-Go zLyE&5La4~BMJ;LT3&&cwF?*0Bi(hPX!a~s?`1YtFvIb9x0ia??;~_)f6)q<iVph3G z-DGAJV$Xm#NQ5=JFcu{Ky7a%epfpj8z0jEz6WRY6P;y@7`S57K?I3NO-wX!vL7QO1 ziIvEVqC+Wtbq+YW%jUg^CT%eqgY?PfsmjW9?8pOVJT&+Zn9*y(x}+4Pr|F0m)<OH{ zj|0yEUf5pDxi%I!`K>164LDtBN#!e^_6tp5MLVg{(&3e?0mR&?f=iR*v!--ec%kGy z{!@8wu8#e=to$r4++(Y^tp*W-zzKAwS99|`A&*seSrLAC%MlJy)(KS)&G`(hBo;<} z_)cEUgXN!Tj(+xn7?B5mf3m;Dg|7riyx2yI`y~KLI;Dv-XBLRDTR`T8x}^-mQ|7FX z0Uy?8x3Q}+gxBhH0XK1jzL*M>uxcjUFU+I|hYR}<m3_@d`8a^dO;>kI=fQv4DFLi% z^?GhB^DD;7e_29;1aR2Q8-icxOj>a=4rKoo$7S1hiY0hX{=}o#lD=#IMhW|6nWYt> zs5R$Q@~m`!BdN-<7|_lb?$A421ww$P4e@Tp-JnS(OTENRH4R2hnQ=47=1ELjtuqBb zWWG2S4Z6Y#y@w@`zE|1Y{Y``e+4F1zwhhj3$=I`ymidEy5mFJKJT<v8xYW~g6U>N@ zvZw$hG(l$-`hzMaDS%<Z35#?m1LJ*<EBTb<be3-28Q6uj!y=%j9c<W4(J$SIZHqT` zXhq-0@Idq{(K2ub<FMs8aKvXS-qm-PH#_HJ3K`<Lq$v9NbKG|;Hr+Bx@Hd+2H2$V( z?v)iNZq*Ff>j@p<3%wabfrr`npDCb|Y~j{ZHO>HwFBP!wXPfIUPeJP0nCc{?;-(M+ zGlQ}?95ecWU#BWOOmcztn~%-<eLO~faaWQ_$|e0Ay-H~n>Ay>AA~&sV|2&(4h(6pF zi?W-2Pbwh&m?dFYgG6d1`1?I!`IVC3mF$G+d(Bs-$@E@}m!$8fO=D4P`uA<+C^)OZ zz<(m$ST@sV3x(E{r$8G|@chNMt1H`zLR*|@Vk|?AvFgn1U`|pmt-}B@-pcesdj%E& zsJfnzInp-L;ci9hU1O=%<<<-SZ`9&I9>-x${g~qO!^G0xBbX!|2^V_ma9zAgA>ME8 z2`B^c*WoRvq_4BW#G@~`cOxE^YFBW7{scjmMX27M4m&DQ)A6dYajmS`+U)(eepB@Y zA@EU4X0DQ{9r7$_MG)f^OO|}#dP>@lKY|o^Hsb6lh2Qvkd4%7&(Fs{`X&#sRiCnZV zJHFvD@538Piq7CO1R-ADwL8NI-!Nm8>%tC^+Z&kcFO0oegQ$U(nzf$4zt;=X3*_zL zu_%Gt!u<0nt71W5k_Qgk^RN<j=7YmXrpH~_F_qO|!1Kfv8T;(wXeWIJXPqq!ZvSc% z(xP=-$P>+f0^@h*i~bnBJW{aF_)YLY3Rx4eKp)9l>O8T=Cj=yVo^!nYE8a`4ty1{H zM|_~q7%#RNL!Um`Ts_BAcF9^}a!vR<P7W4TbCZRCx^LQp=bU7E9rd^w(kEATpY$L5 zYB-_UswkEB5+4y6*k*v4WvVNV>r*VVYGb84-VA7->|Yts8k?n_=O&`2!BxO8h{MIU zcy*1!#vx&-6_3RW|KlV0rKA;+4tgaxhwaJ4gz@=YWn{WO5dfDI8HqVyWu89h5m;{Z zYSfqH(B72-NecQ%4I&uSiwdv?)toJyI2QG4n>^N_b+uZ*E)?LZ7;`X<MUv%W@0>6} z_j46=%h>@-EvE{sB3V@eu2Dm<N^%?ZPC@34*TT&@nn&Kr%0@&Ux>S|ElRLuy!%IYz zTY@xt{%ZT_-DJO1{trh4?zH9%Bh%*4$++WV7^+Z4|Jv-g8OOiAPP7o2>Tc*W-GXh> zRwsUeB!rIokB$~s_d$~8ikO<d*|s52L5~++qf90vC=DVML63X1iR*U5Fn%9eG2Y~| z%;$>6VPo6Mce>X=5<EU>Fjc#fwCIZrARRu-eW@RRG}sV(!?zNhcZK_ytMr!y$}UCW z8M8I$vO`l&bqQf9KiHV{SDm}WECEDqI{e*WKY#&DH=J6am6Sy*-1p5_(kxJ&<C>z+ z|3iu<&Ztl9#y*vN&Qp@Ix95FYJGbZG`1@9CIkHT+#f`cV9vG`x?#K_jd2iaTCUqM{ zgZHC;*sCJ)Cxp-!LukR0)mZuL96`&E7&1zs<cD89L%8s~2fvq5)=F-D==4Zu?^8nO zpj^}8r6T|k`hGYvjgH7BSW45UOtw;p|NFok8$HYT6DiUaqrVDi5_uD1*x0-Zy*iU` z-?fKcwYLyuIVkMUx$nQdyCybJn)e&^Ic*(<5sDu*b5ox?zJM&8UD+fiuUGU{JThpt zP5+_yJj%ZeMW%&jG76^zCLc@ro%+;FxOQ0AzCsRa#y+0a#DsuU<pU8Dpqbo!GlhPI z&B)gVac-NqWhG6BEe@4uChOwFOy%QrW8ihxtI5K90zA`K(yYsMbG2U#vvp{(Og8Dj zkt=qAiZ7iyq8OXcXW@+LhPD4ZFsdmi+)@h<VSM)aTD!%Sp-iEs?KKpnNl0ysW&`=% zHD2*m1#)l+`D9{-&w#Zto(7%@nt<X362vrpgm3=@v)!}o`G3ZS<)6gwrSK5`yQ-Yb zOb-NK4J+~&#jy1=4`SXPbG{!uZ<n~lc)^{u8^o+<Y{;xxXRGYFD}a;fA=pA}V598e zx*7t8(OskV(Cu5uIsq?kD?Uk51`F@Ri=+`-f9I>1y3ARO6B^uWOuWp%d!y;Tl_#W6 zdfsz0y%?DbmK5Mfq82+#uyMAV@WK+_EyUCH<p4uT%y7npxVBUNGN*oCTd<JAdzh%3 z=AQYhm0lmPHx+G6@Uc)?`KViMSkf#mXvl2=UzT{_8yiNgk3=Qa|3Y{0c*bVCtfYMQ zC3ZA9<NHYGi`H02ntIaJp${rpCKo5~TI5Yl7OBRSBvn&%{ge=?o^3G?BZbs$1Ha1D zNpi$QKUsXz+4X>}1$WM!@{d>u#G=migAO&S7ia(|^X9Get^RevJ}j1Z#=087$EsT2 znQNsSQn97uUA!Lp2#vFjX#a_C@r6;gD9eh~DV2hYyXYS;TeA+$t{twtiI-i&a#w1| z1ii7yNqgT(Bpm5ef7IF*;i^En*0EscvShtO-!+ef{8{I&N1QxcX<$U$)8$Yk&9&uG zBc=%mDx{1}_f)Wgf4^V8XYH`9*8lwU0UO}goM;0!2^9qn>&VKmFag&@oK4@dDn}pY z^nJgI`N1h)%Z`pg?2TH^_w9c8;a>9_|7C@u%Bg@M)Lob?7gZ;pf8(j>>FiwfF}soX z8oVPj(=dD1*-d-<Cw={GSKBwnJH56aZ;x+yKQ(%fJsmo<3Ic%ri@4rIxvG1C6HF45 z$@c4pfm;qP$SjgO!<PZ(Jm33JbCF5ww?3+JJ19FsJp4fOqF;)z2l#S%9r6Rlo}XY2 zAdW`Xrfvya{yEf7s(ijGX&H#zPGHez4|P2TsFF;WRon1*kS2<2`)z`h?t&OySTxGV ze<D9)xQQ2FspOB1w>=9S^|SnF+CHuPkED&>!}nqKv=)Aq`TOw7CuYE{7N^RK!|E2M z$^~birV8x+_2IVTvYWxw7HItsDc8>I`wU&U+nV1oSwxgMT_QzvgPa)JU*Bf*zy;W* zwT9p%6c5<?m++1i+z%fxna<wT_~yBsXMp*(++I4VDE6<6Mz9K3(wY=>rO8qI#`1dh za#~5f{X!r3J-Pm+0Y-nz<LE7NwwUMmYGJ4rVHWtR2!ZT&;xU(zT>QUkY+QyLs--Ix zG`6tc4P!34k+xlXxJ0q9g)-}?r?-VwBpHS%o0R@oIzL#^K1!y8`He<+3$Y^BsGBBt zI^_~=gpf{JFyr(zJ2<#TN8cx;oPT4_r5M;6-M6O@I(O`-l^R!O%WbMvZAfZ!%6IV| zb#!-ZKzRt7Vh4VWeX1cD)mqn#Yr>A%!A3P%TIGbeCmDN-!%h{{Hx#=L5KUI5AOyI7 zv$4H><LYZDC4k&5ho2cHN!;?4xbR1Sy~*PeN1%QGj`}W-@A2GQ58s|l5QjM0G1i@$ zEOxO4UgYuVCfDBT=0v71mD<CPTv%@-_B4vyrHO;%MrE*U{G>`qu^?Ltktn&cct?tJ zZW$$H4yrZlSDT1|5_vmYMSPpY!doZgh>n;CA;2}w7GxUyf-+{Yk#>tK+8fOESLtiO z5P(!NlO8q07{YfIh;DV$&JN3qOg!SMCc36kQw?*q6uzqv4|=8HH&_(n=x%O8@NfA{ zec@~z9?}HA^>9MoO9r&sf&~(<?2|o+h1@)_VeQ@z*m6bji=*@_E>aLp!s?A}Qh$NS zwzXo))$Klk8?zQva12Mm8_DbH^FLaBnXi!BP6>~*ZJ`Ots}^r(>27Gl^M&?}HBktR z=n<BGO+BqDoYF5P{lonI^X;A^##xA%zGRuu*^2Ox{!LqdWImLsg`M!WG*u`lpee$e zFLwAm(DV5^wV2{Z4`vHD*_nO+ByazT-1uj9ZUu(M47fjsbJ({e)~j*cDSqkD*xa|K zV5B4!Gza`|4R1g}6<akB#rX0x`JUoP`>osigz@qB%eUX3E?lm~?U@D>TyC1#!wHaF z1E<>)%A*STq357@Ke$5)&)r?&okz>&4`5%D#RNhC&L`}P-dzk=MGJZQBNTZN_*g`W zwr?O4CYM2Xi>&NSy0IbV7?oR)sU=&**B!jW>h|m1f3k5=GkuyVG(BN$HwzdJt&+tq z`|1b2_f5a|5087U#FvZdyqL|^+Sm(1wne3^+%l(N9tkD`N~`|vSGOR~>CmEviRGVn zSLYUw@KcR@?bR@G9_e)dkI{!PJk>uM%MI#xloual>SxfgV>UJb(Ec*`^snl6LBHL0 zfd^*g$AIXQLWP)rwYaa-$BjPPjQP-5sAjHFlLKPP5E_|uQPSKk!GxI+Gkejb6r}>J zm-|=ZOa68mQoSZ#PgHJNXNh>X%+9mB=B}5(>sOigf(y4l&<ssSNUaMKA0WePSuOAF zShI%VrC<(HefD#-chcNDM#Fab&2!t-Hk!yw6)D7+%g4NE(gd%e)+?+}D+;=BQQ^Bg zJwozA<0wbgX*iDAG+c?gUFLSjydN_fGzQ87x6drrp$_4Ry9*D@(=U|?ucCLCf?CCo z6kaCkY9Vw8OvHj+*|E9fWW-Vf5{H@6uU0-!D(1h3T7yjns*1bIMWJ6KSpELcz^s2a z$x*WJt@cjXpCpH#KEUrkpV)}@2hY6W4SD>@&Hvdy_U~37`*_CJ%(HE5TO#MCw6hMi z?`y@zZq;5&Gv~41_J{Q``xy^L<@rs|GQeLUhkfB~Aq}}vo!lE8Ofuwt11_>rk?F7v z!x)@*<gfxjb#L@zP~pQH>M{yUsOG!^4ule+rdU{K>a(xmE19s{9P#{x((S>P?U=`n zWTg(l>rLha<&Htm6F$zo3G2N7IP+Wx9!!KHbOta7^H@{?=!wpd{}*Qgn~$aGKd{`) z&G+(d#|J%k;SV&uCVnM7__m=*i899-$T{zu!#UK9$7+iB5*hd=gi^7Y|DnpxPd|76 z<+4Oze62&ilG-&UCd`D8R0kt{|9C&z6gb-VLZw+8T^72M8STy?pGenux}QX}-DQ@D z6fk?ZWwOLiah!}o!(w4HQn!22xII{3fkqIKtNpy2)jrAtoxPo3JGNp8tnj)zNFk1B zk1$lcl*ttfqm^H=M~~z^JE_W}&Rey{S;^NXc*q!e@o7lJW2(nOUUkyM>^Gd9y%w^| z&Nc$y9zDHFl!_R*@SJ@AA#fu7Emi%hyIsp6gPr(;yIsR=nWBe#DA<64xP;!mtixY> zKG{#5jgj;Q(?QqI)ZcW<_oSqv84U`(aQhR5`yvBIoiI-LQtR8vOE#?Q^IQ&wJG-)2 zZZTrK+n%f`lv2wugg2=B6|!2Tv>yO^uqG-C+9AhRsmBa+F0*jUp?wzJ^bSmkkR!j) zaj(T;XgH@+VFEkE0Fl_wlztvPkhhvYsjQ;tqwI3BU<Kk|kes0dpt{jm)tZ`hOCPVR zPHgLMkQJ*NvoOY<Y{J7ukFXd2n($M>47~+`b^n)4zB@t}2TQoLUyX4zx!v>rYm|Db z6#L*{(nhkpdttKb!nVuVUs%#nksP3WrNxg_PK?ZT-E|{|fx4hUMe2|5y|1^<6XiQS zz{MyRyiyeQ++Ub)%wrxJeh8!bC6z@u&sh2S7>v{(t)+#<fjBpZ;`Vr=vTf1yJM%8A zNrlM}<y4I1cnlbOu+Ez@^oJqDxHISG;;BRhLv@*VPJ(@7C+e}HSrFM4o?Xb;yD|wj zgSqa$BC@;}%JsOu+*$Tuy}MfchVm26hA&6#d`oSqfAU4EDZKV0f1X+&==DiLUhU_m zr+*qSe05b|_BfhbjAaM!8N^CU?(u?cQ`TNFd!YtinWoYEth?s1IRt02AgqH9QEY=Z zwlmL4)i%I?WRfvb0lP)_6$Q!p7O!LOVpb#emF(@1rm^1^aC-8S3C_H#JmR8J9U)>3 zagu~vAmLrkEESZ9EG|n<#qiL`3=NDw{P{eT=10tLY1(&iOa4NQC_6S&Mo<cyb1hh* zpp-KGpL>q&zc;Q9|H6(AXLBrPB_Ee0bjf&@b@E>w#^^*y!Sve9p+I-g?U0d)_1Pu% zv*VKnlJl5>3ITtf8__)AWiF+r1QMfNw|-RZic>WWbvKp2oe?3>QFG6^q_Hw8%ZtUR z5`DZJK7)Y?Jgy2R1S~*4=XH+RmbSliMGin?5$){~;6>rZz)0J2%=0WjT@_-BQT%Hu zoIM&tCC)-q9QSPzp8wS2v<WIh&sZ8%wKB^8i+KJ!HYt1)&S2QcrK9v?1x0r)-%_(I z%CW%$H2X~yvbW(<R}j=>1E$2e**IAj?7=b$Wz@Zk!6;HS>UapVWOn27p?ui<n|&OW z?)AJ8E8g~c^0kyUvj3<8ntT9D{2&fyQg)~f(o2)Lm!B<YY0^%PL;|nLqapU*Sj9)M zJh6Kj4*sA%|4$@IySTk8yY-d*&!8E}31gL&c$g`cNj(CH5v8ZO?xSG~{AMV~Lc{Ke z!zUkM)_+a?b|ylzd0a8JN$zaEB;G&sM#=79N8GjbyV15%BGp%iB+L+;yoI(mN+S`U z>e{cFZNF2GJvCiFA&0nqCGCkDl`V53$AP=S&hj6Ra3oIWQ{g@-a6W4LH_>0%uoV~o zBjyNBc2Q^&K3vRH-Y(bm`G&7}6Bl?Q));fN^htwhq&y<TifS%g?d=PhSKzKmc91nD zJ|qJ#Z`>l4PGegD+iinJ4HxANxDAPhWfb>UHQYyh!R6U0ZV?;8H7-`rK5R`%c#z=- z%Hv?4pz9CXcshOMSx$RiiVRc3hu4GreIXi>7rIRoxK;x4hp?1&4I8*&>HV9ZGc=e? z|Aj;FKBP1{<=H})BZ^(!Ie<`n0Cj<<UyiwZG`#ihGBg~qEd7f?L|GUb3AJ1u>UfSh zMx@hiHw=(@NYKzVO=LNIjp#v$?YeZoB{!&kb<|L_Mjx0kA|T}IOoF*XRb#rN-}YRi z&9U@Ry^%Y|^G*(OB6c!6a4RgYA@MYE{dnuP30v|D^wq;JsvG~yb^uW9*Gdt)*1m3U zO>J>?$3@_gQ(>!EMjufF;g*TXDvCQBj4U{U!E>y@oIIB}9{|wUD7}-`@mI{U{34Z( z5b_@n;uOGsx8XUtm4M|ISIdnvRd7yO|B-iTt)VFMu<-1UxS1sLl-*n7>aL)R>M3x^ z2SOe~pB*pcW{gXc)cKQk>>QG|ENv0~uY1|t_(x3{dXx|2dy-f%<*SdpCz!UWps_-% z_PmXxQB%>0mfJ?dW}Oc`*+%TqjF=V-6G%fdobOd5G-b-&L#kju+$|#oFq6jsZy<ki zQL!U%h#5!|t)C-Uzuk!8wxL$HH!43mQ)x^R=OiB+FOwYVO;f>ClUZ^#<9k8_GF3Yw zYfRyOR-7PHB!a%p{qj{q@vwqIJu1!j|FQQKa8Yet`;?%9onR0KA}WoDbaxJ+BA8%; zA|W6GVh3WPBB6q<sDy$b*oock)oWhs+Fo`4pLJ#i1_b-QpYQ+s=69dF&&=S=IeR^O zt!J&h&z`q)t=z&@-@2*$J@X7#Jn|yrptjRXdyA2a#2Zi16ZdcSV!=KCzphWXCa#fo zrH)x5FX3`VYW&*8^DY(do&2i#{rr-kyMvec$kZAAan7l>Qh`e+t?^bIxN%X4lJr%3 z>2ZH;bxsO6ni(v!@6_u~fxg2gv|sY+;IW&^=WcX<vo@>UE!(nyKX0}O==ARF*)abV zDKF;xb@$bYyX&=3#`WC(cQoH@Y!dzVm4KPzrc?gua%OIsX@l%K&6`LwX~{9u4fn3K z_6R-ZoEB9$@=SOPhO*(o0dB_<TdhuRcd>!yiF$8*r<7@n50X2U6jLUDG9qw=!r)`> z?VE~q&|TXkS5I=pg4;fQJyh$2bz5>febuS+9}54vJonU<t<Pf4%=WLp;_eIC;7+9) zvxnR}CuZR|!T*(>XI-Zyt{%#+H*BB3J+s!u@4R_o-M5ZsY;7<u7EQnGdi#7GQ?FN@ z*4&uC@U_3(rf_Mqj<-T>)!dhu#N=&=J8_)1dYp!sPTkh=nI0DKH-!oP2dvX|vMI1A ze&4xmkIZsO%}1?u6OSrgPcq!u+wjl=%}M*QMcu`~gBs$PH`{hUzwtQ#J+G}V`<C7u z>2zhuqc;g@QK|#7CpK>Qe8x=gHyd^ZN*uYKQZzs3j>OHTD?Rr8bxQT-2^T~4H+`S^ z+*{+{zSpahiA|;{W{QnIa&v{rRoktfyd+Jxn6x%Kt2kh3*}eyZ+m&32IbE1;`LOY7 znK@m%d#4po;=Spd6?(0SbNu;%I=xknv|1D!u{*eQ!|r=i<i%HhN{DD}-Yj$Fe!Ucn zzT2h3uJ$T7Yg-t2r;o4H#qxqNZAPVbEEy?w=dn+Vn5?nq<uyuk?mhH$(3tXOy`Jr# zKF?$2>zc*9ANj7{v69yv#~0RJy?D)(7m7!66X(uOfBz)F+9Ec-PO194dLMY_E|fnH zXz@()QKGn5%k;Bb55#M@r1eI2bRuY{{*B{u^<Oa~v(X>4p%#lm#NrEH&lzlYY3j=< z*E^KBNVhW|lVzG$zrJ0+#ALm}Qkx2-<W{J;rD`pm{AP6hhtl_&E%jU=zAXPmx7D#$ z3y(FsrOsRLxVEXEg=B!jI@hHW#K-U79`!Nn^&{s$CGFr;+8a&#<M2J@#AOP4NoC1R zR~kgUXBh_U&MK|Ji_U(snIGr1Cw9)T5>2s{T{?(~c^?lb{6jpk5cjiaAJ86=(@c3u zJDH3O>%cP&<D<s-3|cfIHZ)Z~vz2lk`G!T!N4@Q?mEU>!wpK4oT7Q(%t;2iSv!jgV zU*T<*XV`hzp3s_`()R3hqnO?&PP@fS7Ym3>>3c5d{bMtoF_HB(%CDs)Y#F}Ot6VJX zK@a~1O0RhS>#w-xzPtHPm*vssz5ltYnepD$p-*|E()X%5i8;jzIwoxrOpBH^TKDnj zvb(91&o`4C<1FseELqGqTT}n=$J6VQuJ@1BKH(`Vd*6ItmX%xAJsIt7R>!sRHEdPC zN!NxN-3P~?-O$9+vDNS`E^%AB+|ZwG7FRE!v2*;z)ptg#ZS8RHVE))%=GPiu)Vfz- zRFJT5n)lOA%k7Q!IDCj#f8akZJ|NuagV(ZSs}dC5u6>e624yfpr`Z6H$<3yT)qQer zO%J2z@AmAwG;3aZ_Sn|pZ=1ZmeI$C5A$<FOvH0<w2N-ViUZ%zilnHov|3-=FqT|6{ zF-zMAHNBd%(lDaQ>?pfe>ob<lQ`xe$zxlCv{lK(o+E?q#Pt$JDvAlodK&fGp`?`xY zEgI@EK=M_5*ya2IZrfjsn)4(`ar(o%8-j<Ny|ma})}ZWyi;2%47Y^;r*)w6~AC_-t zird-#xu%!cq7^zzdcW&tl`qk(rL^4J#1YT0UR?V0qj|QbNjsg+=u?-+0XjEj&MQia zDd>oc$3M^fqt$Ac(zB;>H|eB3G8RiT`dFI$Ea+h4=Btw*Uul>XHt^2$UScac)G6w= z@Ran+tx4*;$7-I=%s+i*^AM{BTN1AgQeN@#s?@~|jUIa63?8#l`IWV{>M@y?4<nVs z)@^Qm)6rDZGyL%RR?Ec4-2U_4iiQ67rx?ys&9EMNU#@IQozfQp*_wa9ypJgpy|s(F zY7L%O7O>Zl#=X2)<3NeC>-IHnowacPx)*EY9p~JQE@&I^Q6gSpM6sVm_}@)E(yrNs zzwA<O(zJ!9wTfT(f~>jkf>ZY`QP|RTU|negn};@^I-6;=sykYIZ96lY?Nj25(q88q zo-Z)BX`ePh!y~0!=V`*3jm8oa#q3|boxLo_>%+{DdrvNW*s{v?{$ZnLmP=x8+e`6w zoGEsAEZ1_SA?41h8b%*?=wAQhWb3edFL%mM+`K)me3GJXshrrb&Gm+rmL1v=kY;@S z)Xv*BCmi3b-#eq8ol;%5)Z$5*OKew%Y&b2Q)qaA@l-8}w<9V*Oy_Ke|NFMP1`01j~ zc@GB2&mGv%U+S$=Zi3ki=_{dHQ>QGy@keW`Cx5;)&D+?Z``;tljV?H$mZ(8DG%j4# z$wWMZ7wcc|Vb?cHtPR6#GMY=Q^e}(^QWm8O$!BTvuf>a-zB#(LS*!H-iFOZyBmXMQ zeZTTSiTku0%cM-V>_X?Bkg`x)G%+e-m%+r>??$~@?AQ51uHsZlUpMJ&mG!q<mH8^H zdm<h#FP^W^<WgFXXTt~7-}H~{zz=owJ7&wz2u@%4dZJQiG1-m>Mj38<Kdj@5Qv>>S zow}uU=Rf@S8g0Ckwf72d`^f+;cX?%t@V7$;41N`OAide;W?nKM4F6iQQapM^L!Bva zRO*KqHHZuD?ENs2_o>;9ciRG%j=k`xLuj)JD{;2=CcDOcSeEq)6DP6}KApyuOLXp( z^1}0;tL5~gh3T7mYAxP&w2|dVNx!labCs5)nH2?z`F9T5?2y>h!@{m!u4YzXeG9v? zzhp;B>+kB~Jh=a+_`b^KncFkwAJ)vx95*O?)3~kHT2oqH7!x<D<EwQ(@`-(p?SBxm z#N=d##rotnmtHSCWGOr9Juf-2x4qFW_kWz7GMYvolQY?5Y*){J$^NT{@43kv)sdGP zTVRrPbIONj^KzW}Pk5rH_DK65znB~Si}$9TvppJFC_iyz%MWGec8g7)K4s@JGXtJ) z+l=*hrujd)e`RZjMNcjU9}mkDcUN1X9o=xHht=vJ*_+vWJo=Z-^BAJA_N~s2B-=-m zn;&jBdiSupXH_1#<$aPVGkDq7v&<>@l$`kdWq(ODc_n6jXJm%~Jzq)c_PTLRIp3_O z*P{D#eAPnox}>Zt`q<nqH8G%r{nON5@65fTUZ~|A9NF@1){`rLU+sG>$IIO?PHxH> zjf520tq)Da-exboaL^_0jcoRZMYp5}tM|ycQHQl1Wj@#I#4|?+pJtL`%SK8{h>dq! z*W!R{$)46z$Mx^mP-||UxS6?giB74(v?KfKXx7WxDI>P}ql8jszZtGgo$Bh{n#}VZ zQxZ_W`>jKB2goOQ#h&~4*yY;F`)7wfaB@4nyi9I*;LuIt;T@z7KC2(I;6Q3vQjmA# z*mJG#jW#$l)Y)qPyth{`-{tiSc(~q3*U^>tc#ZR&t;dZwtG>DT?!AT2yIX5#FF$zo zd5~MGyiBv>+pfAWhuyMMo_ogin)u{Y$cxN}2WPAKWG3foEW3Hz<w5^*S3gC}$Tt~s ztw~~c?PcO8>v;{!Yh+e>Jz6|0dt6CsL4S6<(Vx+q3tMeIq`Sm(#HgY*t@>YRXA^uS ze1KC|8UG`agVTHTI{q?rh?>&lb$azqtL1kV@9wqOez)Vg(sT7~k2UY2ci~0zVe*4l zly2HL<+{wOdf1WP=An3bVub%z-Aj(;0Xv6wINtjFn{k<v>aPdJEhuO?(O|2?R9)3| z>VL;Sl8+SMw0`iKTlM>nvAp+UmXer6YH^3@33f+r_#BP6&{@X4=%wMLcfI~RIK%yo z_@Rd%&Z=XGf3R^|s=hzWlJ{riZc|y=3ESQbcX(PPZaOAhTdZu)q$OrDXNx3D+~fu< z__VW)neC0Fej$%<L|w7<RC8#5I%Q02aighWK8l8K+S^223C^#-RYp@|Xi67XwW(#N z)yAjGEI!rg=$Ya9t~yd0o79^Hc??MJTP$_#%!TK?YfbKotEJS_Y}RW)=6;v{kK=cr zH4R@fWssDOVk5`<-a|ST-9DIna#r9nr%^4$#q*|49y5905|_*FN9=ny)ep)rb6vkP zr{v?p6N&~8iW25OssGp2B~2{uMJ_zv&eCo9dGUd&PLJ$j+w)Wdr_GHW9sFTmVQ1GT z>p$f2w#$B!*jVzk$CdoxvPOO-a$;MY;=L7*m=DKQUKZ0zXfKwQjuPg!r|JLf)PEXV zW>{~a*xtPN4I0_Kk;@!+WAvfXmj;^IWgnRLck{Ombe_o_DhX4t)5^C=IN$p05$p6` zjour%<_wWKF+zHbo@ArGe+Ic+PZ(5x#j-k2Y<>Tkqba$rd;PnI>^3+jXH0siYIn%3 zyHQV9mA3lXy#6Cw*4sU2S&!KrBeP6YPn2{&H1O_SnLTg&4|{(6@}0U4DFL=_OY~=V zZTQyX$cU(jXU`A6aAjuG`{s?A_GGf#^ttn6?ykuCxbah1%A*L?R~yxzN&57ZR0>^c z*3zNP{i%BO-e)dBQ8OtlD2|;kd1G|)RLSlB19)Cdx6W2NcWzozn#`%K<4QdqEtMWM zbNGvRUQ9_}E&GWLqVs+2>-*0XSKA=3RDM&we2>o1ql+$ydu31Gtt~yQ!DR8K3j=Om zeD}m;bk9SFQiC*2#pB(tJ>IhVUE@3V!<H`zHfXNd&|pFSJ>G5i{I^n>=VF(|Xe3UT zH#?nvyp_d2KDy2RXuD{JdY6D+^-MBC>Q68`^6<k;b<dRaPl1~j&Xs+9>dekF<JI>0 zShfD}VvG9g<hjGIiD%p|M-qHg>gv$LhdbUsld4ruY{*WT%{O*Ny-72jcl=dbxq!oq zHaWas9h!FkYLr;vH5uO9<Okt0c9YgD#7_l{7}`N2&11wa|Fu%fdW}!lZj$L^B%#%C zu)?so1pP)EU*8&5*GWs(VcXWbVjCm<O=X<p3T7BQj5*}L_~hyKf#=i2Q#U+(F-Go2 z)+0mN;P|`urj44ovrz&s!_Ta4!@~pjBu)y|nxwFPRf?N}Q62xKKKc_Sy%gQ74TGl6 z{qX9;%P9s=O^k>09{Mycc+-E~)8n@Td|pp%Thgbsm|IBGAp_e@zSBmj^T|3xwTHYf zxHe{O$iZ7O4_libvK6yi&HHe*;8F6GsFEYyCLhQ+xN%0=5^+88wrZW8ukeb?s{1xQ zP+D!YzozD53CG=94VLFOJZu#|$l%E2H61%N(zj~$yxy?<_8whrR}Aq!Ju+Zr>f%Rt ztx$Z>SU9k4Xwv$Sy`cl0#inW*l=gD5xG?+Kz0U6KO5!i?YcXA^Ztpj|y%r(UF5K0) zd(dUV8qJ6gPKnQz(#rE+^<R_`A24~*`{3yz{Sy_(kJA!cIc#Bkp7_k>$@T;C#Kj(b zYPbFR*_iV3r>;ReH*eUv{CtM<>-Lf!*JAJQ2sAY;cq(}`e66LCOji1BgCUonr+Ap@ zuZ~~*-dob`n7MU5qmsO(feKk;lh2$?TqQoNbJ_A4mQv9pTTIS2emQaavtpUiF_uO3 z+%)U;ym>@jBh8`cq(Z{Uf$oxF_6?0Mo>W_kVE$h3&r^yfvi`^Ru2o!U*W&d<NwG<N zAB2wm`|O(+dvn@_XiNKro5s(1yx^=xS@c``hi439ANfDr`7&^bn3;jY%@LB<-1>@* zHja+Nugqut6M22&j>t#5BiD>GmU)<?@Vc4ZYO(Zd#j=-1zRnyxpjEPqdgBof8q6Io zHZr%&Q%uIX<&rgh539^B6QAh!HhGt4Uc9+Qbb9QqC-aBL%bz}SX-#&i`^SNMqmx$6 z6;nTS^r^vz)=?j_lgd3$wW!aU92&l>j@D!)v+xnE^ADS!T|G^?aM-%Ll4i7;4J*q= zMcjF{!1;O%@7kMzdt>hWse03MyvDMN<Icq^j(-8)tt~FSUrKw+JPB>t>GhKJlg}Jn zT_m%)v>@c4?r(y27F<2-7o^<(^7G<mI?p`RhHU@KBH~f|Au<v1*}0im^JO~xv3TI9 z391U2k7MhE{w?EXRi?8+LEcn+!Q~B#X-OMX?JWy?oITj~ne53sT~aqRT5>jMP{W3q zn>s7CyE!t`_QV1)3B_|u-FXLd$}-wdy?HhM`e<+C*I64E-E%oMr+r+OLHJ#d1(A;> zGL}!cb4%4$(SG9m_jy)6x~pEUU)fVfQfcN0$%RH%%6r=fJ(@JQrPwpW_XQ!>@@^;n z6IycRNnp2ged5Az_c;5y;JlnsqX)ah??^{<C>h>cJN9Fy_FcPqomV)_jgmg=JyNsp z<a@_^WW88kH2I{QO7o^eo`+mN)-N-s<EIsRA=n6@Ue|?#_Jpq3QOffiUH9#P&JI`s zfg5Bd$}KuP@a;SIP7j<z=bf3|dbma>4IkOt8h5A7HR_}0a%^9z_lWScq5#W_dtGlv zo{R0d>gjrqcdPPti#2yQO%5q5Z==M-hS%vI9Ve65N385cv0p*lz3$TxPj>b^b1J6s z*w{xljb{gDmcL1AKSV#oc6FS(cI=hphkq5^8`fyoABstVndf_+IcU((WTuosv$6O6 zw!K+3M@M3DpJru$X)lYvJXl3MxF91??nuea<1ucnT4YO%80T5{ZfEG2DQg!lOgv>5 z9W;q2cV9gIpX}Wp6Ym!;UC}{fR%Y9jf=4DU%hR43zMFac?$p;S?Tvl9zfEW$-MCXv z4>K2wK~mSY+!Y%xsm$U{PCWYSMt;fDW&NH!xL<O2(=o?4=dMi&Zqek3P45BmDUU2x zhsbKj_M7+c+H8b>-_9}?>jp}%KDW=Wb;qR=%A+%^;}xHa8^kE*ANXTJq-DZGnc*gT zW)0T1E1j9CJ1tXFGA^R2`M{<t`<`zy#Xv3O(}s|_&+c{I|2*a10heO8^mggfpB{C4 z(MG&fqE1+=51qQ)Tu_9Q-aO)Fv(cLI<5fDFDevvlVNB0KQhiFYUmg(CIJf=r&0U)l zN<t2YhwMyTy0YWg$h;ht^~#NJmL#>$D*7k+!NbH0x%JlJN$;ZF+j^xw-;mQ{nfb7? z9IuFRk}0|>!$apuXZIiEJk|V!Sf5QwYZsTPTTdU<e&RYY|7Dn~cUEbl)747Ky+M|d znB8v2{LoKUQLq0#_2GUSRj*L9+k4Ya*p#WND%!_LOgy_Y&TGRzW5ET51tVMTD3shh z{HC_V-)*eq6Z-m{eBtr&O0ePC!l<Y13O76V)X_MD5=XG&xyE8{={f4B`gCoOv#O2% z<~Hk+g2z8-TkO>G(2$!$#|>E5*0=M9_MI1qH}jYxzplMXqlLB^;zlRsx0$z4S!@2* z#BGDcTfOO-SkG$iJ?rMVrb8M}){~alc|FByPpah((|+AgEbnr{&&06z*uVW`PoAmM z&whf(n|dFH`rfqFd3Rmc<3@G^>66R+a?8%uH5@Rm?sWN+H=1iHol*L8`3bSiRgw43 zr*HZPeH$>&v1nD0spkXZO{se<ZtRJ(JF1oSPhzj`{*N0i@V@=PwPbGLjdsb?8tMcz zy=iChEK}DnEyK{nH1@&U4=>-XkWxKhAG2(eZTiP9&kpZW^)fxa`^{=elT*@?b}n;T zn>VcgY)JdY&6I2hHq;-HY_p_^rkGvOPQ#dj_7^iA72bXCbZ~X-I;VU2v!fmqq+Yl) zdYjeF?Pr@eu{VE`u3}yG@$VL;^Gkix+LXF2H;L@AcG3FLx}CZ{R*TW-`rL8eoI&r} zl+9g%t2EIYB&B=m(7F1Dx@CUs(P8?CItM1bDIQfc^5`z(w2~`th8p(lpL#s($xDxR z{oGSRw%FKP)b(E~HzK=9sdh}ZU+Y5OWkutyrz;gj2j5n|=-TS#=4DSU6Ry<R^-mw` zl5ruDZ<|~mASSz}ONY$0;!9=jsArbzHQ081tNW(1ahpR-O2YK7UD_UGyE*8}@lioH z8fsk2@1AzN!T1M%wbazez1paaqsQ6tZmD<8!~05Q&*<CBv~PD8xs-cT4_Y<ueXZcu zB9#m&UaObxdKvLggE!|zE*P&Le<NPMb=e;?l#j_>ovd=YzF*+UTchr%D%m7QY`*2+ zN-JaPwsxt}+phJK)=v8<xy~slC*^o}cCk}W$Du_=uP&7vX{<T!62A7B^@+i={}eNG z=u>BMa#M-<K4NC|{TCT6X|*RK#c0XBVsGr`nl6*QG9W!|bGfVYg|6@Zkod>R=2JvZ ziSF4qn%=ix+3eXh^v1q!R(FcpG`DOcH{d~+BX#`(&E|>!-O6hF@^Q0As$A?csU)~< z2j8PlwdzN_pPl>Q@CsY`7^x2qLpH4R-#^#$MbCI_sP=bRAJlmliaE|MJ0};qMQ-=U zjyB0-lOmUzpA9+bVCg#}vTdEdyzIFWCS4!&-+8^xF4<4(U!?Z#-gEukgZV*;3MX-Q zp#RJfb@s{m#U&hV>~|p9dR$=gWMy65Hhuq^yUHeW=A^41dp|52DHZKguG%JJ*O`#b zS@-JdENC#i)C|T`w{&;2S!&Mfoq1x^o!+slJG5Ea#yv>Ia<H@V`$+W>*++K9kCJ+8 zF#M@QY~(D7>q9%b2j-f~W;KxB^7q-<@;fF<rl{#&xjMMbaG9~kGM64c>6ATJGM0D5 z_HmR%EbpQ1_AWPXcPzPrTQxeunVrkVLTbY%GnB2CTI6>BbApXtqRr)mD=kx&U3xZl zy>Ws`{ZBi}jrET&p1b~ao6G=doZrY<eYGa9U43%ItifV)LK`Y*#qYRS{Ak}S?fAe{ zvl7En@wZ-dG7)McHXIz?Be}nHlXEJ6B#m4WmbK13G)evAZ6~oo4aXkbzqIk#T~iwL zRmMj?nx!!1!ylt!T3sz~JIVflS%*fYvZL;o-cB+<7WDFIgT8g>Z!#33UZwsilh>x+ zFsaK~H?NDmSAg3X6Mn{HA6s>{tKxvf^G!a6TrQpPX~wa(|A^sXZ|!2W*<!$?E;x>- z7sTm5B|7@w{6H^IPhe>=AK1BDObFZxDOMfA_ox;A8{uc*rT=^U#yudkGue*pDh<>F z8UT%fW<X1z6(A2N0BwM_f|j1s>-0YT9sPYHKo+PA)B)&oh1{U;`@hHkTn~uan`}e2 zCVOEQe9o@QXvrOF0Q!ItU<OzLwtzk0C<1y;uhaWxKyN?~=nixS6anH0eLj5;eII@A z|M3Bx@E?4TU#4+LW3nMY_Gkx?t+Bv`vz-H455NcT0|J0Sz)&Cv7zSW1=x3nU>3#Y; z`g?z%FW?Qh0mKngfWAi+pzo#cr}Lomq4T2i`#;}+PWXin>Q5SHG63~8`RI-SR@`v* za6(HqB6|%1$hM<_5MYb|W6=^qzX8I(75>{`{2Thb;Q)O;eGh#f`48d)oku5t;s^N# zI!`*^|KkBV;U^w6mdMBA_Hb^Tkx$nIEC6?a#<tMjWFxYd(8lAM*OyM{+(K@FhC<`? zp6#2iHCAbnYogL3&#Xhs9TuHh71J@Hmr}FM+HISx>e{N&(soT`XGu#*MB;PC)d1oS zeLj5;eIN0RzMsy+1F!<fH>d#Q7wEj{{OLOW@A2(DAhbJaH_~X`029Ck=m(I09tCjr zhMnVKzm;THr>+Xey?V6$!%tiBt-r1k`w{flRr=(s+5WLx_cj-;Ra@`vrPyo}@kl~K zEF9k{$_r6GkbejShzqU&oiCj?oqt1s^yvTb0G;rK2lWs26ZKbHpa(!>ihS*0U?dRI zu&(rk?h1|5EIYT_#nWha8#ew#b`H=Jg)$4!S7rnB1fb(DLGX9<Z#dw2#pell<fo(b z-o0C!%j7dUw`jOfN<tz6=Tn^z6m#f2>3m6lSOZ!BX@$nX|M38wQ0+l+lKdpuo%)IT z%LecTXsib}t}7jDq|!Wx#`b6S7T9@UPbD_Wqyrmo-<eJG?8+AO@5VBRYO=K>wOGy= z9kz9>F53~I$F>WwBT|p$h3RsyuOF?=Rt?i+%LZz&IlkRkoU<AWwd%x%7^-l*3E=op z$qC|HKOM!__Fd$UsI_dk7{m?3c?!88^b5gwEf63LX$MejXb;eJ()Ipt*9Ua{q?76c zq@6SXTY&m2P)0%`w1-0D<<9B~C;RIveh3hbae=)D_flcw?K-o$uvhkQEmjb&%L>Qq zv7%^wwlk(DD~>hbuq(b7+ZAuXN)vi<D4W!aalrFk^qS!PU2z7iBo_Z}LQhtNe_t4- z&k7^;*=BHucrn>sjg2<%zy|8W&)^*V1wMkf&_}ENGl~_8O=aic+=V=#mh=MgAPAt? zMb}5yNx7sfAO-wy<N^75(#g#LRlp1&e;L@Mu5>i%Ga6qNew}O%j5h1YW_hc#tYNTs z1nd*Vx7V(TeEaTBG-P|H^=A8K8nFX&jM<^NChW*OQ+8y&DLcBrj2&HQ#>xqc%-Au2 zo*!MnfB#6bDLa^C!Vb(bX8UFsvAt7!vod@hai<u(*clB@jMrydz=tLN-PuH^E^JUQ zetZ$e3OZlU9&N98lW&xYYZ2uE`2?XqpzAaTx&X}q${ndc{x>{`>%@7|cH|3t0;HD) zHK{KhjhJ3SahBho{JstBt-_7HEcoXgG`=x5OUT}n4A~yoXg_RyD2Z?1<0%&GRH`LA zlWxV%t*~JivTfMKHMZ>XT3dEyy*<0Sk&kPe?AbLs-e8B<?eM-G_jmO7=T=y=(`i=h z<T49(e6cylor7~sz=_^$_hjNjFV3%!pUEAk%a#o2!N%KmW`hKNfzF+H;H}Z_PrY_c z*HqJu<PS(Il0I+;dI3~hqW<{*-4~GUN&nE8?+Q?i_iG_59c9-=e!rit;y)F3$5@ZB z?!=Z4(%{B<5yqa-=6k0bF|r4Z=MzgUI9s1zWy3D7Bbz#~8(W;%tz0K|yTFD0xx<Cs zEpladi{0405;yi>mpgk<hL%t&z%EyIuNZ%`)0O>M=)&%7cjo^6jV+Gc=UrZB$LRBk z6UUcWz@M101G9}e?omwHi8w>CVpFg-o9U&_h_8ITP{D(Hl%L>QMg0NgE|fp^2dE#Y zKd4`*fBt`r2jt(VW=e4$TZ4+ZVO#_mskGSSr=$3$((Xzu!nPAz5lCYlaeRUSH{SM6 zh0SNf=J3}imszp17~2=u*s-gd9oWrWXV}bz-Glw^m$<WsyFJ*Wy`JnZ;K_a-dvXx% zAs%~r$csHYj23uy7{^D@9`VBK^d9{k|8F1f!@u3<fzJU~z!Boc?H$hS<~ApGZIc7L z1b=cCK7#xMageyMdx{||L7bo%kr%GV=J)Hyh8uU_@(-au@aW$5YP%-&X5rd}JP^hN z(zxUc^Z?2ish|FTiU&e}uZ-(527J4;Y&geTv)!Mh=}1QnfbWcTP-Uw}XmfTa?N*v# z2%l@r4kw$j<0<gznPhW}@y(9xRvyM_A;vA)yUc^L^<SK=Vb8<f0{fzceV-lmX3vj# zv%iHX_hEkv@S@zCy`b08R=iJtf7FXTCvFg5zzyQW!#y7C{w_DppZt;U#IEB!E`ft* zGp%uc=Fl3Z&>lvd#w7ijkKAHjUv-WL{G3vWk&Y#uSYKKy8vP*T0mTIJ1$_XjC#nFH zBT&EnH$TXaQJ&KTPy=im$<!I<sHSiT@$nOl3(C)f%{s6ZLo_*UMdQ6J5&8{wKQbTw z9b^66D#Ygv80XtuFb>IAV{DdrVvO_HU;Di<ZfOjAb9i>d2R0|b_M9EbmOd5u8^?bi z`vQmu9E5+v=gxC{)>HTl;=^O`?61AV1$V>?SHucuc4dP-JGasnKEaaXE9plrHXxVC z3)5q>eY&wh22`Km=a9%3p7xM$v>bgR<N)~s(ggzm$`Po)sNeou^F*QTo8lU}1I`_q z%T4mtYX6Ay^@#6-4OG}1A9dty&~&i7aJ(OxZ_ZApS|jE{n{A?4PI1{CW8ITI*iC)! z1-pB*#|ORP*L`4j0ArroXNT#y5|w-q@&c_0A4IwF9K7J3Q!C05`g`~Ydhc)Wfa4ap zK=FdO@Nl;$r!W4<cSd}0WaqPN!COmq1b&Nhk&?Jx$Vq#$btAP|giU8bJ%Q5(pU4-8 z)RBk-R6iO5xBxu>(ggo)93b11j-eX3Cg9dvxp|gQ-_e+#;Lw?2$2L}k{Ehl&5B%n# zxfI*2x$%BwqZ8t~E4#BpVD~Z}d$h-kJ=zDm!}fn2KnpxM-~%4`z!o9^4~Q2P+#r4s zM0g>3Tq8%whrr&V5ORg{D*_)wyd(cZvF8DJaIe^%-O6`iSD_Qmt+It)wB&NiJyXB~ z#I)_uG)w$@FbE$(ol%KVO-sH(-Kpp&p)Vk97zB_e&;qC*|66n+*`D(HR)8*QwSF$@ z3di_)yfWtlll!Q1{ZJf>I?UwW$hS?|vBebMk*BS9VmG$BvOl)FV@&bb{ZcQ)U2hJL z_WE%4{|g+T@y~}3T5y0sT&M})#%CNMzKHPTd$_`JhwM*$LY(1qiMUAT1b=|o^yf}@ z_6Ib|mGussZaKElob5+lp%k?z@&!~=im*|w(1)mXywPgYWCi+6$bqpq9uDvT(uLHY z)UVXPzw1Ld?&)0h6<biFuU!LVrIRaXuS$8yfSw;;2r87(b8lb;{EnpzB;PcHG&W zBKXr1FXU+67*l}2{u~eXdL!;v1NcCGK?sf?RXFnRaix+s{P^<(G3hZh32BoDrJm5J zZpcTS*~QiNT;4)HZub-;Ry?s6+m8D49A9-tb!*bVq>0Qswk$;7iE@B+A<cj21Jtj- zs~;fSQ=B4SrK8eZHncA^j!@&pI;pYTP+hKeQ#Oe|=F1mbVQkoA%saE|Tih__Juq%~ z?A|UfjC*f(zf8aZArFWPA`tq88e4GWVRfF+a}l1<@vq_v`4`T|P+X#8@=yEVyP#L@ zLa+Xj=L(JJh+M%IIjjZhlg7wv4Ot<4LD~=vemvlFfZ0M5Ao&x{Cy*u(a)9z?>fhf{ z570T0R?$>%)o6S_ZN-<I?UC0_^H2lV^-)(hK>f@Z@z9(dUy3^0Do51gT-l9W59oRx zZ0`k)=f&>A{`Yoy^FchQ3E;$i5pceu7M>7iYUGH}uhhhoZ{`X={s?0bzZGZ{#3bSX z>6ZJu;0vH-uIIS03t9HKek;_lOt~5+<%%0H{}gQAu|g9#tG78NEm5%!LC67$1*8Y4 zpQ*opmj~skO@JQyip{3<7u0X59y+gYceaCU4}Z6ND(dmcmeBUL?80g%Xe2k}@t*9D z9bWMHUa-G6#)>!l6S!MaGb(r?<cF{YNAA_c6M9XACv^Ou;EK?<2>s1N;y@`6*XhCK zu@~1muv5!z*kQzjJ<vdu_vT=JGSmVb5afthe{iX;ltdKzmEwU=54cl51JvKYLw$hi zR8+fG({9rwMyT-z87Q-5sEZelCw-4P{WQ$O%(r5v((T!WHO{EpxntaWvRm7|*zH1Z zzWs~Pf&&ENLKO&jLEPZ@K^zg{H|7c#g9Lu&0mlLG92~fn=f*CrcVwqCY|$^~7#l|5 z0BQx8Pl&)=f{+8K8D0_b1H=K!3n(|F`T+I&|74wzVtze9xtl`c(ZV_|<@K4U=j=dD zoMPO*>6l+zXbsytpniwkD2Hs%W48*t5c9p^`@P`<(1Hgw;STYlCj1w1<)Kiw!2giG zxw{kG2L~>1aAIc#9H81E)jji34~cf@Qjr_bnkErFK)E5+2W+X|0qTFU!+-ig-$`0j zp>uONU&Q<86z>P>tFY98mE(Tz44Pxbd|HMh#=R@Xy$8FV>&0&70c8IIZ}0#B4~Pp@ zAm9aYgX0Hr<O@8xU5z8V;cFI-H(;Ut)tSF@2j*nbj)RY_G7Agn#^yyL51VVwj;_I6 z5wu4Yzfy}&p}6$jF-SE2P@Ljo7R5Y_fjoD1Wur56upQ<SEV-PBVkL1v)DPIH%I`y8 z3-bc15d{FW#*Eex{7>Wr6nmN}HI=pNtEKpWZ+j)SsDBS`+>_7WJKc;M_b1Z@w%_E* zu5aaW_P+u9-^lZ3H}gLS#|I%VL|gEKc=9E#oLJ||Li%=Pit=&{=l9K!k(OjVIyGUw z;Krm8T5Q8?Gj=}5?b~9|k8lMXxLZuIz!PHu9N0h{uxCf-TOd{%!w>Z0=9WV(1)31^ zBv`+ihif7ZkRGI3F|CiHoPhM;e^Lu@F<-V$9StANc9(@ZKM6JNLX0Uc-oxh~UI3q; zhI;5~__>Xq@cUlu`Zn)M<a#3xhyZ*bPKe?raikWW5O;)Jxeh)}Kps`UuGBZ%9DVlH zzwOwr0qg6a!sbQvWCxM2d=`hkHwF<`1iIx<vOoOJ?Lr=Eo$iPQF7T7~>@aZvTA4KA zmNB~Anl{n{R3D(V%s6i$2PiLe1G)nLDJ?*LpXyMGsOxQ}`8%rTPQ}`&9axW39BT+I zW6BQAvt}n!9oe}o=$s9nsN;LFYg@ee*ow9qs_>#3N3M%<M0hO1lXDwAn7OX}S8a)N z{^`GwZ(fgiSu3$66Aak}=$Pty<uiVW#-tj!atHRO+=uet8{6EW37xUdzz%tm1!|+v zgfRwe!zgV*eSp8_uvLS)Q*bTB0n&p*0ULnU()=gtgPh+tQEoYoKZmEx##wd7Tqn)d zQ$5!N`90Q2EOTUMR=TnaYdtacz1ZbV-iZC)>`D$=QGgRytAXQ5HGYWlMU*QSH}RN3 z_m)3xSM=R?e}<Hl1T)lV$zrj_^xz67PNN95%D3nhjw_tzB_BXqh=+0E0iEQ+PA<3O z>LfH5S{QA>(g$nu*BwG*@p`nqB{F9ejB}?o=RJXcI}Xs;m+jWNu^HBA{q2u)p?Qo= zqcM+zbKEt_7;7P{*s&DkwwbQ%+-gsDalIGDzc)AlfCCjEF4P9_<1?NRXR7gpp8MLU ze7D_j4Zr-~h>MG{p50oo86mpt%m&N>2{i53^hzaPytrKWMjnsLkIrX1qef(l{xriH zRd4`#^mJZV_*%>%!{=IcYE_Es5RC)0hEA321N>Y0K%IK^#M=32wLd4+dufAc-kxH< zG21)Ck{w-yI(M2YJDUX_ujRr1-tcMO>>@ZoJm3H>ToM7tjq3dP2Ck$}G-1^5xZd9i zq$QnA+OsW5mY?Zcp<b!pQXWM4(2aax8xMNWot<6fgn2O=^s6a<z5p5|%o?=;%$<|( z6<I$(IpH9{450N@|3+<)G+#p}HH8_%m_HqB?04Xties@ZavIjZ%)@-=GQ_Wy9vJ&x z?EG49Z~#~*0P&zEfEUD%DjXrMh;T%BEab{X#FQ>djeje<qkld-pY9!-usLJ(*_EyQ zeCIQ5TP^>gyohq+t6MzTrFCvx-h5<%73Pdc5B6f4;9r7zcffoRx90q*NPUp<0Utnv z>;#a#e&s`2O`(xCYCJDUi_%<PF4naaMq~Yaq6ynS#|D1iiJi=FXJ=ON*ts=c;D9$f zzXkvYs-p@og!~X~YvPH(j}T85MfCoxuW_-z?Q5OdH)L6frj@x5X<TU8nzSvaZ@`x; z$d}Krc7cwzhZe9zKO1qrHU(?XX}uizV2h5eN^spmEf|PnsteIQG*lD#RS)7)BVLbo z1^ir3nWZDwDvZMY66E)1S|QhSV8>Iz*A<@ZOqLfrn@#ZMI6yr34B*8%0XU8jPridI z7P<<*t*y~lwVs=8ueNOOa;zh*N#BAK*N7K__papd5Ia4v*3=nsz!r0cW>^Dl$jzli zVjU610a{Pkwo$#A_)g*gX+gSoMhj>J{3;(HZPiKMQ>gbRI(Fgw{>}-wKVphGJ1`fu zt0mB^=^pIV3NLngl{b8V7kGeH1ZQzfoTw2saYXo9HLjcix8&;8skM)B-M{sFjpU?R zX1wW_^B^uqqP&TRe83%b6erXLtkK`5m_zS{wPZS2KgVB7h&h65xaKf`>Ow)luk-;D zjT(tH>8q`HmDVT>#r<%bux_g`ite2<W&37Xv%?FWSb3^Dd_RwGf8xN(Y5*Tj3vh<x z2XRCI5snD?@+Ge9UF!N<+ZX-+)$^0CBf(aH1D9*dfw<TC_P@9eJ{7ri`4W4!f3^kY zRQPd#?s25r3+XYe9a@R+75V^LN303_3LQ|#rCXcfe9foCk};pV9hz@vtP$!w78v^u z>}ZNBJHFhLoq+vMW_YoanUy$&WAX_#qB=)}&%eTzBWWJLrSAV0yNmu!izap1;SAKr z1Ugr!Z7XtQ<Urhe>!GJs!?$L*up<j>p|wq+1r1Rj*5lTe)424}Xn!BhUg!fz2Ur7i zFTu~`0+dHKz}(Jx(tN|AJ99&!`J!+SRDv1CzAfUu3p={h9row3<7r;(c)AxmAq2Q^ zLI92z#EokFsKS-nctWoWxglt~DE|K1AAR>#KQP!uwK6BFDG%awFmkCiJa%@a8!KPx z2p?dD{x{+D*Svnvf>aX#P#ey`cM5#~<pQ+-fE@6n^#DniZf*U9V}EWR8vD4%CDs^m z$cpVxa$tuSxv^u*JfZz~tb94_Paq!D1n{B?KZHD?_B**UHAw%feT(b<?f*u$o)kN= z(z7y0u9gGwb7#t>Nl$xX4&4Phzz#mZ40eDIKn^yH))HfUdTF$~gKH)qKy_idzmVn! zX}uWTWBnr!8I1jM@-IP#g0UZM$chup*`ArU?BG0{%VH09Y$=Z&UB&|syx6hSN|fWc z0^kMlLli&572=VVZrk79zUcI?p5wf+hWy+|m;+VMg9NXi%Z9$nbcYUbLcVPcA7G4o zr3_e-Z?}rNAlBY3!*`Mnq&+BH0A)Z1_}+&E_PTHqj{P~9+bak+z<nB6w>a5~?Vat& z4$gOj{dw%j5-;d~U}+5?PSgYuo)Bj$ctXe3cv72RIhf+XdUtRA)&9kW|JHvqz)`g_ zClcm7pSAp)`ZRn+d8#Wr1Rqc~)g1OPWZMvv>7G*>r*!W*u9so}tr4Jk0m=n`L<dSi z^XE`p8H#({w}j|{YZ&`+W~^))a?vCgc5s0QJG6+$4yO>jI2=LC0em1{R7b5mA@0<~ zlWIPtI#-Us?>HGM{?_(I|9|oP%(N7$<jKMuh}w!gNRT_9Ude;La%V>uJEHDl&5Dr= z>_GjU?mMPj0698CJzxgDQ#dzhf#cS|_pA>PH|)@=CH6sjO>t$CiyD9IM;oz{1WUGi zCN#)gH;jKz*x!pCS_n`p#A0xvCI~q4tGRLn@yFfx_m~IznyqVkUt6{L=lL?}T#-D8 zYAW>n)C%NS>7MKu>Mc}vDVuD@)nI9kcu-G%J;1zE%boZh@&U9*mIu%gpgktORR>CV zX|x^1`F_-xGKXrR=0x|oo5J_ovOTk0*uG>Bc3?h_9auo{f-eBTgPKsm3n4#5+nRVn zuYD(1$Om;-Y4*i&hU@<=-|wm3sv>9pQoejzkRP2)N3ONZ9WlTGYlSS?&WZ2=$jKwD zRXH7qdrO}PazUCGq8OkJd`}FJ^wU<{Pw`}gaYvRvP9Hu7Yn0<GSm{&;#C}({Z>}dh zFb}q$=f(EV=i>m{&j4N!KPosP!j;k_54I@8j79d*VIy3-vk(srmNdeM<xFy@^d;ZH zl~pm;OhQ8Zx3uw>xMy$Bo}V*|<Uo`U)s!=zKt1II>|dVh!Hz6)Mh<R+eFsdrdfY-k z!Mp%sfGYN+!go>(7y_6A-<$`yb!*oFapWWAl{0vBf35-B5sfuz@z$(#niJbS+nw!A z;(-HR@B=ktKk=d_fFs#4w#-Vq9h0sjS*xwuwvc0^+<LI>)1WiH!>@E#{_pbvA-o<n z=gZaWDU>JUwG(Lq`!9Ay4sORvCqoCu7-AnYJ#IfD(rgZD@`qt_$^&SPm?J=QLf=vg z67Q$oK0@gG*ACal{wuv$;RMV*Bp}{QcVW9{d9Xcmcx>++FYurmz=^#g;5Z`S$)349 zHrQUB)sd8_)$TaQD(}^okz!GObgTLm5uZ}suMBlmuk!D!f8Ou-d`%qYP6hc;MIKZ` z4kVfv9ZU6uwsvO+lO0iqwZi%UW7w%Do9L*<#eja=imwDQAQ;z7_YbN9-?CO*0=|Dc z`TkL+o#6X>;$B^>KZ&(q#fc8EzdI|N$zx@+yx5-EUhoBc>;V^Q1vpYX(}S65{CK`@ z(xVOA1>V)@Q$%9W^dW}-RrXhH*ZA|i`D=L)y${WMB*hao7-zO;hAn)vDOU?f!TRfg z82>bWu{JviHYW|xAJ755Iwz>EE+!4%|BQ5hBI*7xx@X;(6-`8(Npyn$_h6+nc&wB- zFw=_<aDn(B#BLmO5OAXi|3<Szi=VbjudecJH{uTY7g4_=8iN)EoB#Cppr8I@pRphq z_eFgzXRcOPIfmzyS05((&v!*0Xpgmb=EwsK*~Va9Hb9UE(0<3bmhaL4zM6`LltYsJ zm-u7vt?*u0&yP9BiQrhG3)?l-gYB9Q`%m`*4{8B;Q7Yg@3I4Ws*IzeRALQ7*($B!J z?5P%iRz}$V$87hrJaI-{h0BeQ?;QGuJg9;v0=`h~?Z5(PG1TIAC0arQ7_sf)J+U^Y z6K6lSZf(xv97zKV2WbCMx>w|jb%6dliW7uuJT{HeLrm$-wnu?marUskD=V4m$x5bq zp#`WFU>8~r;KeQxc=Yb{OYKn)dLVa_Yen1<#vf`a{%o4)^h>`F{q@T~ud$pAr)~Em zf38W}epXMZmID!2gg7|ggYBCQzdXen`MC-5bOV;?s>aoVV9U3Hb;48!a03*9FKR%n zC)r7{Kg66L`|~5fsc1`9h<@EU$&D3H_GCLJ^H}i|#B@=B55>d@5zHQJ`mfB>hPY`} z#-3_1s9>`DFSpmP;DCHHS!S=N$R_yVp2*qIF}3B)wQ1XGIS_H=AZjoB=XpYhyC5&O zMIFEl_geR2O9yE1_ZWg7@6<wWKF(jL0XyNC>Oi$U#NqoN2I%r*fBrZFjDK({+M4Zv z{fiRZ+0H~BD*|>VdhszCZ4D^GYi*l1_*ZOU+znb#;A5)CAbMTq3wb3j{9iez>Ypo# zI)u4qTQ=TTmu;BnSgT$UYL{Adt%$a*skX8|ne6Y1n!E$*Gv=ID-z1^|3>2Gf#JQ0M zq;+B4fm+vxd#Sf=O0l2v{$$_oxTgd6qDGj){<duUL}ykQ?*Z^wVFED8s{%zht^#ml z)*#b=t)Dx$X;Kk)YKlKy+BN;xz8n4b)z7xkQDkeQQJWLZiE7X)6}huWzWgO^`x#fb z+6&nqIoUKv<N%h4)kd&ePZo%ETBPA@I=9|~b0+)y19Y!otvNs+EyZ5^`95WqHc%7y zq8qaONN_6Jj%}ad%C^UOupMzcKEQ(=)xhzBxB*yd{CjgUofMi>jXlCRB#1%2rd_^j zBlN>B{~H?fTx=5LLe=viQLQ4>D^)lm<jXg61-TINp*_fp%TSjqPO#$YbX2bkwd89+ z*KTbt;94mM7zFeNngJ*ceE#s$RvIYOe(OeHZ+x<UgaylwvS$So+*m=ZC%|I`6M66l zA^<10i(qq<+rR3AEqb&^3=+g2YOBScjnOVxA1eLJ?Ta4!;%AO_(X5&iiR3{QxiaY$ zkzARMD|kZ3A~hA!;~IIw@rB3L<4RE{El#k3{Y{Xw^<?qbZ;8e~_FMc5=PjHIG{bRA zpvLte*tcaGX+PS(VH@sQ$PF`Mc@dT@Z@dG`k8xx96FgZSkUxP}0geyE2@%Bk{CjKj z>)@WdB_nL$V=Chh#iA<l2hS7zjG#ULb8`Xa8twS`59L83`A_xZ>UyQ(`Hygg;|}UD zGdxk3A^Y3G{)pFDqcz)`zb=e^%Lcz^6akylx?n4yRju~dRoqCN35NF13+;*Zg+?ql z+=}H!I<j1h>D*{f4tdc$mKXCm^6?sR!%?@x7sm;{|A*d}mXc%%eR|g#e<&V_#-K4S zI{y(4v}q>CwoP>B$D|tapz66YX_ngZWl_FVaHD#iMT9F<XDOZTiMos{+X?#@#+Y&S z`lSKL0R(lx_D#6^1BRjBX<azoA68=xz+YE!A8G$c>`TbEzcJtb5l$?3ygN9+W822_ zxR#DT19*`Ov}!E-L-zd=FXWrZvH3%+xY+Yq{GnJR5`Topq*bDLJsH&uzVz8Y^PXJY zI&A4ko6lm9FeX*Wk%c;iS^-b0=gdFEm1&;fo*Ua4@4$A%K(9p@vXw)%EB2YC->1Pf z(maqoKx=|)vA@y<vOn!TlNV}$@o&Pmj<aT4!=2c+2zRzM(vxkC;GyMX8(I$F1o489 z68P)uZ@pC8u=QcC6){IN{^X0q9nlyh)G5T3<)iFa&(89{+!v6>NE(9Lm>?Ib5`&;i zNTUdmJJEwJ2(o0cKD}6!S5G!Iz?iMZxDxV&+8^Qy@n{NaGKm8FM_VBN8?v<{baB5p ze?5rcw+G1n4qvptp3(!dKWT?tvVW*4+cwUIZ5ijnwuHL_o&c{B;KCLGI9}j*gpKwu z8*BBXrPv5NZMIF|SGc%Cu}6q6#Goo15$Kil(e}*ONR72`(eS6wQK@A^mO9Gzvm8h? z2Ia%%<J|O^c869>s+KuVmDY{f7*`#ZALm)Yl`m;qAz!L+g=%uJe_@;>SF0;P%`PWI z5Ay*0HGziw8h~&fxNYs@ANO{`{)w*YuzfGAD>TLUw`E(xTv*OH4}ix<7~1LpFLD6$ zZoek)(^P83mJfHR@GC<9!nHNU9(ql{6KX3sB0R3fm9&xe;FcEC?<CLU>VA8!K!59R zuFkf`cvQ`Ss>PsicYW5n$+yQr;^h25)LBW_ifG#^K819yh)>}kQ!UOD|Hd6PS_gss zd&746++JLy=Z%z`Z-dRhKK^x;-jV*B?hgBpCHtGPEunTSC)AZ~hW$5_{X@Obehwiw z+BE&SnqPgHI&7q!4$G<LS488EF#b?mQw;iEt`J`q2imY;M;&IXsm#>dHD}Em)@KsP zA1J49*{DA2-K7l+b<txRBPb85%zwCJkr=co(v6vQYyU(3h1NxcyJMYgwYrM%xu~`k z@?;0qTjF8=csI5q){zyAx5VCI(Cs4jH|@|OUts^Q#eeQM8D@EP=i7fQ+20;x-WBkK z-M!i7P=MMBRB~a(2v>#$hCg)eI6r2n+LkR3a;)lG1b(GT-1$lj`Zu}qCBL!_`H_aw z&+SD+{YSpzOZ64fx57HycG!LgV#oG4*gr;S|K6zG=;QaOXrBOOCfHN@8}z@@2de+f z@azWtN8{fd{@(%q-<@p^gPp^C!2@5|p5XHt;(0$-^Hplmm?ip{;qxlFBs>=S6|SY2 zLq4c_>=8cyALdG^^RKxliL}DZ05kS2+O}XKY|qu@+~E6#_D9`@vp+xn<9BhlV|)qo ze_9J(WBmuUf2{XbVpBZ?@qe5-+ZN`?*L@MNeRy9S_vPB0@IGh-2tx~uw5@vmRDY?1 zoSnvYO=u!pmjzkru#H?y5%no_{5coC_S$D$5%Qv@_Fv$Nv0A$tud}9W|JrM9n>Aot z#(PxMS-9GYpuQp+fB5w{)a0l>7v;#W*M%Yeqkczwankq~tOFT}?=%Byt^dLP*sq<| z`*%hBpnJ<K**41kBE5iqfFE){KenYhBKjc@=*N6}_o#CHK8D@d%2A%oUaJ$UTZf;6 zQ#@5~+mhqT`mwMxd<usz_?607Q=KEi=f4M6NV9&;uHW<ia9gd)8cQwOmTGc&geX_8 zPMa5Q$<=Lhf-CJ$znhEirTV`SP;31k_P<5;k9AOk|A+nQvm#wk`|)PmqWa@p0?<~6 zKie7wjQ3|A29;|so5<B;>q7ffaAjjyUzR%DooyV4*93f_HYY;pPlWB~euaEXj*utR z))IgCF-gdY+S-4QE2dq4O!w8siOvd5YtDZ}>Mi^loCj=#`fQ{FetXUWzP~qHkNkgt zVEpSVHeHSH2mthf#z2kpKd}FC%KxHlQUAf*57|F261<A?g&zt)?SCNJK`eKI05Jnu zt_a3B8*%(6zdzmsu_L;6t`J|gj_=P$BwC<0zar$z@6MHhW*WbA{F4@3Iog@4u?Xuc zHEUZB^q(tywLN}o%mTi<H_Hyx;p%_1PZWMbITbdi`#>}R+9$Ne`+#8oLb87t?jy;k zxj)qY$*<&3=!?4RKwt>VpEy(m^n3`*i{*oOG2P#hrH=3dR|Zvp<BJGS_?#Jle0=~o zBE)|`R~8Pk`=$1Wo!Iog=6t>TCA|{i3ERW|VfOI-7<cg9>4UT?>c3b6G#8&m`vG(T zYFz`4xuT_H|KUa*G5&kOmmrsncE#LYKej!75Zj&*#GxQz7%PYms)j&T5XVP8+Wa_C zp3resuH?lI25*1~gs*dD`3N2xYOT+Dt0}QA&<`rDnljZkEs!%RvOdNdY)U^HmP3Bz zU-T;#TICBdNV(;&-IpTL)sWM|RkW=j2ih#qwOhhGxEkHIFgvbhlZ*Sw=lAWwtpTL< z9L?%W$H49$fHF|)zF_`3ilL+z2led0a>n5ID>3(90Q>KV>xUZm5Vm8|aJD0HL{&_} zbG*J?hy)yiAENjfu4DxJFl)`usKb2yo{>h#C;FLau{EK6|Hu5wa2vf}>H{Wv8h<et z;`4%Pvo35aYIWOCv!UAE6nFk!p?>&XG1&a8wLkogSb&bABiWzo#;XGLu=a=S?*jY# zq3#&Sil&SNMzg{y7=`D+WFbbf9g~4XAw>A{om`pX@5E$fzP&b!OW^vxv1~mS?`!jQ zzk=BFt7FjGF+PlHn8I`U*>*-hYuH~{YpJGb1-00mP*1K#yLGG$#=i+?|0rAB3kqLO zdj;bhh5)9(7w-Y|)9%ob^grpw6n`zuBb%}9F|_W_5A#$(Z0EGmta$nuwo`~{IGz^5 zp$Ki!R6=l7ROd<sPlzwIb0yZ-uGTq|Z|PIgppl+fyHu-Q5yYQwjX~eR6(^lvv;K_g z&uhkbakUkG4%DYIU*_^5AG`-39&8Dv@h`N$0UKqE^<S{Rqni9t^w&4t|KsH)CJ7Dr zmN+oes|V&0aZgc<6YSp)^VP#x@r)2wGBXqyhx@sN2*vB77z=&~@jYA_UEy0Y$N6In zegC+x&I_tNEf4nPV$o+YNT5|nvwW^s_*$h>+t$pL+VY^qf$l$52NzyP+h&cB6Y*;+ z!W@XpnXA;`P?tk(mgZ=;Vy-qf)EK`RfZSg&_h;Ur71jQ|fp5D14{Jcq1n8--7`v{B z|Hh~(I$+&*A67gq2>0KOVI{M}uzo$F0wuG<@tOd$(9R00L<L8_k1O4k+Wa#AX{o9D zMZJ;}GX!guUD;SyGd9Y}goSxnu$cqVmlK9m>f0aSihie`uLZd1bw#~}Ut8hp+Uhwo z)#LhMeFo|?;Vzh~w}$<V*vcWu{a{0i<vMMfQtsyjeAE5ETn>PL7uXv)Kp6HHin4|M zeX-se_MaWfb|po!(z)Y-C_a*KECPCe*Bl|hlR4kVmE;k=zs&ZyX4ary-3q_L*DZXV zvS7G3GwI%u(V8;)yXufez0$0ECpLE|=6SzI+X}cc3mmHcncw?-hQC9V`U+_kp<c;F zZ6z1n*cR=Fx~vb@=eQ#G!&*J${z*QS+P|f&4DAPI3Vbp4<9+S{`}-1a2I_ZUS;Oew zUn}^3tTCB36t)ayrOD%2*}NEFf&g?3BnuIZV?HW4QUh0ZfipF6#Z2SZ#7<lc*Z-sg zNSkn)MW9#wtqi{Tz9iaT-a<o_ZJrQVyI$dR?TAXA5MPi7b#4FSdsDS&D9h!?`GVR? zMZR1`uTVX%KWa2S{5maaKUB9HZ_B^`3wuJ|LqB}i{vZ8&bZCj3@e^slxxTdTiv=sf zy8q&7gIVdEv26GJXtrm;L?DjsSqN~)6WMOG9OlR3Z-khDW8fRPvMVWyHH3B-+U{4h zEgLr!(JJFun3v5DeZE4o#%yKy01<6lsaHh(ifA4b@8|GC-$mCwz^Z3u{E4rut?=~< z>M3!EPh1TJb=hbi<Z4dv*Qos<_Z!j+zZnJXXWvDBH$IE<e&KyzHP-#`ew7C_;91&B zD%!p)D~QD2BC#&GuWtY=OM?9u#IU`K;@IBB3BV*li{nK?#Irq%fQ1ABM}+NHxUwR8 z*bny?E}+JL>)cDTA_f)D3}?+6e&6qciGFVDrj6L@s38J<`?<E|av&}b;^#nxQ-Ya_ zeDfO5OZ0QAA5-jEA2kSd6{@X#?pOG=71ZM5hoUAsfEC1e!~RtJH)AU(_6zF%y6u{z z;Il1(@7n(h8gM@407DHsVGaO$4Nq{u8bd!;HfJ>3yD*yVOG#k+mrQ2+mP`?}bSywh zBHkBb65Gddgn06Gu1p`+=ezr=df$mYC*PeG4!<RcKl4WW{q*N|RA|Wxr&rH`NZ(e? zfr@AFbD$YRJiq%p9rU`1)L8g>Mbxhpa<vtTG2|BlkfUQgQlu@X*JdGa8zA_7kS6tI z=)P}l;CuFd;_5(L9RPK}WrK8aPk{|9p5)ESW{+Td7ev9<li0y!Q-NvhKq^430Q>Pe zhoxwjf*(SB16Se)d3|?ZiQ2pRv4yreKmG_}&?t`|^ZnI7cZdV_K_gvTtF{%%fshXw zsD1n1bfi}@!v>(HLbVlsedRO1GL>p@;6-8(){6OK{CmJpTjRbzBiy%5za5L;h|*|# z6X)#;r~op+H$TkG#H0r3D%~d@O>n}VzvHpLP(1DXG8FcYWCxZevO{Sz*uk`!Rk0k; zm*aH-4lJJzeu$!yC&ZWPT!{(z{;`6Ks*y9KBeqNns}h6!ZH;UE+uA;(w%1!B4v<z6 z`W3FNo&$;GL91hezFF6pAm^y8t-!BTtF08Hw!+omzyYesQEh&E4CZJe%vl!pL?+!% z`rTBeCC&N%@czI3^_1q34;X?NkTcc@`+qng2Ml0)=Z9jfC&2DA*rAL$*uT6I^!!l9 zY<4JJh*>!1<6zpXYFw%03Gs!vGIz|NUt%A!Ta>@MAPxy*&m`t)*7KKrer8nA7qqQt z4iwGtBy)Tq)9$SF)q4;YhxD)6uT<1l#&I>c;_1*BlLKLYUsiy1Kl!*%f2s%V|AsaH zI*K2gH>gW{z;^@WfN#=$_`8Zne|;5QVGLN%55M;igZ+P|^kIAEj>7nlgI#B`!z+^5 zkri{<k(J4O(6IoA(H_oBss>T6fFsqovTjQFFR_nKmkw+n7k8@0AiJJgzvT1LNo@8= zzbd|^Y91uae}pYR2ckSEXYyDU>+i<8)==Zp?5J4Pr;u(D^((@<3TkjAGlDVx2V>2* z7uNh@t^@b#4KpI&ufnkJ%SoK0HP8mA1N_h<0JT8k(>QB2E(R1&a>xCtfoy+D6g!+g zogG~@mmSSoAcFZg2383%56APs4Iu<PA->eWmA!}`b)|o<?o0E8t0#rl7=z5TyZtiz z#}CFnceQHUc;rL1<Uy1-BM)*j*8S{UW<vi8^W~azX7Vejx6KJhTo?uW2Vh?g+?S0t z-^-!XgucIL`)0{7xF+yjy01D(Pz#D6A27(EBU?EPdw<6|;9f(2wr{~Wb~tScJGLsB zm1i$x<*OF~iv{f>b_~xs;Jss__y(?+{G^S%t&KkOE5aCL*yGpLmS&CeuU*?#<Uf^p zP<5^tRXdjsZCW9>uAVQ8)K}(DWM$B?R9h=eqVYc*`!V&!{0#2RhVLJ5hnf$@YJVNY zSM_BioncF=_trYsU)|O<Js;ewZBy9)Z^|d5vG3N7XzVkH`%m^HkA(J*!`Mz@<!fO3 zwJGfQ+NJFHx+Q2uk;1)Kz6LD^aAXbfq;{@M7}EE%ae;IDrq@V2Z<#%z!nah9L7m$D zx_!ttPL242wiV5Rs>Y!Gf;>pAn*X0P#OE`;Qjs%LZ3Xp|y{My5{jCgj*plf(p~<~4 zKVyxtiM9U7)o85Rs>;(^Ps;fk0X44k!|UJtKrLWq0PH`=pcBg;X^1^$oUnhvKz1Ny zJUhB#ChWb4omjsVNM$EBh=882UseUj*AYjA5akIS^SQEp(X_8>B%FMWe9%(uz>hyQ z#Gw7@Nw^RA=VFWSd)u{Y&S_cBuW-8dE84b74n+CTG^WtJNyRm&wpU<#i5E5G%v@b1 z1+^9U6fUML!1$kodiGR*t}eGd(u~D8;P)Re_WgC0-?o;kM{|Acfu9=t`1jnSPfz7m z;MHsL0TZ2jKnLP?C#Lwaee;9ak@Q4%e9e4za>Fuqa#K1xB?5YWa#I>Rxlt6$@f_H& z{EJ-a*zU*X|3>-XH?oAWN7xGD&ytwnivGrh{nYU=PyFspb!{uigDP^MYTC9sSGp?m z?-gi{`t%0)l8T(U2Cf|7ctIS1rkIbpwVA`Q7n=|Ku`OFO(twN4^jm-qY6^L{wx5ps ze9Kq<AGPF#q$>ya>crLsV;}GY>;XJ`Fgv(3iXF?E4SOzSr#Gjw(_1px>6{g40cvyT zIUlDsXR=e9Gel6ym+D;cbNaD5O-5owtue^a_}Aoq(o#|^f8Hcc*Vd?QSJbR+tMCO} zDPB04EeH={r5N|s^Pozu@aroFC_i2X|B!-tmt@SrPQyIJMC`|ex=gGSjeQl?Ur+g; z_D$>YU^Uv~@rN~krPV8s2I_Ta2Osc`)`dmG*X)=8A21d1YC$kNnlYK3Sht9s$;n`6 zwyk1kwq~Iff}V?FB|EcaCHO%Q@T3N=B#a#V-Q%-$^QP>0EitGpZ7%j2|21o}0zDm} zX`ydLbuArN*R~Zo5amLmd5~xh^kuG8_!6qKrDFUqrr19M8q5cN*p{ur{OsVKnCrpV zbXHfOd%S3kN3Hw2Ra*4>j}TktkPa9O9k6^TegiDtnbQG>-~*0lCt>_A=j?wjZ#6rc zw}zeLK*s{)qRq{&1UPcGf+xh6N}ep75c=KrpAh6P@GYPFm2m++-~D;$q0jz}l0s{C zXw^ba<C5n6OxKEN+sYiMI#(8aJ6AYgf*MN3OpJfjvF49syJpbXcY)?NV`D7^nqODx zjY{+Sw8n$(?Wu8ZkMFm5)qgRlw`v3M><-;iHp&e52aUHtJqY_x&L0IIFo~Vouo(7S z$<F1kVduB6W9JLj1JqXHT)|qrF9bM(zvc6U+DfjhnUnZg-{Wj*ea+6OCp*Q(otpiM zs?yKjm)A&6mTj05&(|&0wQYr_ty$at5Ld{bkPlgzgj(Bl`2HBSdk*D%-pKiE*gVwa z1`0I4wQ6gc=Tiiz&i~!DKKz?+df;9XAIdQXq8>2AM+ZE#$DBZac5q2JJHC20e0v%@ z51U@tv4LIOv5{RU6o8I7Y~bFb_b+S*KSUwIlO4<F)oAMmvh`U**n!X#x=FWN7nT?j z#KoHm-%`!5>|ecvyN@3iSmS4Y%j@;(%CMDFV=DA*b-luAT7j<pnzlXhja)fKdG%^= zU?pnm%TY&IfVhv^3)SH_V~t-BYJD_E-&d>MEm>*l9=Hy=uji-pJ$%L&A6OewKx@l} z7^<?h!N#lvdjjvB8_15PPlOI!z|Q5agpJp;i$$B+rJaNv4j0kVYn9l9W1x@^QLgM= zv-~q#SsChA@<oIz=gFsh!LOWvU#J7khKu?k)J4viIw`VJuW-7yQrn(_zOAZjp>e;M z2UX@kHF4!Qa^~a6r#YXodOkaniF^_D*4=XgF$ar#{ZWIN=-iE4--)sK$xyjD)%pK5 zzh9jU{`xAN@x5<J3x-&B#o8nME<Lp1frX=4`KqbVfvN1`_BEW%FPCg(d~8EY;6J}q zyp>%l-cl6;o)BNkw`PB4tF&noYvc;&OUR!HA?j1;7+mS|gZV`Lx^-E`)CqjeQq8Y? zrfav>pl!dGD;tqBbABY1tA`xRLhMgP-Dlot=<PnJzuK{d{`id-_+RqF_}y<>+e><% za{YhJ?|;St9p%x&I1ulqi8%;+>_OR|9a<XBPOMFW56HyW-pDTRg1t*~+11i~cBL$j zT`3hsF1xaekIQH;?*cyv0-iK$+^B*BQdpyTGJjnqUqrZa5k98Ur_k}|ex-a%7VD)| z_1<(`Q^n`kbSziD9?P1Q@O8aXUDuMv{RXb^b087DLR=B#$W%``zIFk|{#15gQ5b$p z0qX%!cUv>sm<`2#`c&)br=xgRxv{JU&b`+A`KsIPS3buYu}%D%KxdXR&;W738F64R z;=lxG!3FF>K^AP8!>;VkV^{YSuxop^bGVB3>TW_oCCYGIme1h|+W*(ycYsxOWo?^G zCS_7gqA^7y#g2+1RRKX%x(Fx;(m_$|Sg;E!hz+}-Sg{urL1{{pE`k*Wtk{jls7Yof znas@k-?h&@mwPE{%H;dL`E#FVamu}%v-aL=uf6uFOe>Zqb#YX>ojVFzk?5o$t-K0f zsZ2jelY_j8Wr)VcTlL=ikT3SHTBO2L4%V$bGJmPR1}WuQmT#MoZJVGK-Y-{TpDbu1 z2|VCH9BeDlDPnJRGG-+BKJq|>S%Kf8#v}MH?EC-d)AnQD>;Eomeq(gw-&6BL@PO;A zdk8k`L<c7JMQuQ?&l5uB+vihdQXJJB--j_xq3Z?dbfYj6a9jm4abFcOKp!%s)5>{a z%5e(u9`-_7X^gKlMJp94dui5~AX4Qos$<{${rBi_>~d<9uP9^{mRVHg73JC^<l3J} zEAl=Wv~dw@$oWH?Axp1<eQh52{ut<5-Qj;XkRqIVK<<`w{Ej+bL<8rK0RNG_USs3k z*gb`hz>mBS84ub_LayN4MebCzemtGowTLdIY^Ez&i5S;Zx>0nTZWd=zZE-e^3|Vzh zl!fn4)PaIdmd}|+p(Fh0e8%Ay(hBpISK=#*^`{&(z<UU^New>6nv)X0TDb-_$yZK5 zc6r$v^a`|c89b><!ADq6a`DJk=+YqbV?WKhwVdeTkoRqA#c%^*e_?(*tml^}fp<&5 zZ>r;KY<yqko}a#Eh%5_)T6aSp3TxPiJ*XsZ5}n(#m@XgNN>{TFVcuuZ%@T}%X%5{g z&Bd`k<WOyi44{t^nND7fR_b_*Do^2anWykkT7yP-H1soT`O$}zwk@`94N{X;SZ-03 zRTOKGkX7pBTBer<vh9m#MbHTL#@N499!;S0iLq3^1NOq$P~;Iu%x$=XSX+a7^IlII zYrf{quAh9+UNQE+o4ofjItVb(ie%kj0Q}Bp*!4iI0Mw^n=0&9&Lh0PzrBs!=4Km;n z%<oLPbqeEOc9L#0oX*EFAJ+^g>)=!#epd&-h*rQ;ZZ^VK%2M{z+i$(4Ht$=t_?R+x zZEBQOBw3|#c}0>{Ag}yPT45Tw!uuca5MDbj9oq%Izkw=tEylc=ioWwhEeA*15)5B{ zQ;a?O*4seqv8i@T1B}J*D(k;&>=oa?23<tFY$u-UqzArbk2=xbpn)lLZXeUYcFe70 z%<(L`a~fk`UPyNsDvBgfT=V_g<pmO)#!&$1<d@QllBeVdo)Y4#?qmK5#{T&3EsfGj z9Z!MWq9&^}&R1TYR=`WZJ8Diqjz|OFPl|^h4g4WD^4R+$kGm7?KpotX(DAUo)yF{V zJF8BgTA{E1qx!v99P37Z#<9b&K_28aihTv<4Aw(FN;}l103X;0KEO1Pww<ms4U~cg z%Jb>&nPNZ*-91|h;3I&q#r<NsgQEbMM&x6CT2ayo(@PVya_S_;KZja;RA+bp{L@yH zm$<8LEviQ=jjlmW@Rg=%1?!G-4Prhb?QgDRAAz0(eE$gcdVAnYy8&arTpIg?5Hsla zI*WM^+v^XuoxX5Fr~hx9_iD7@Z>jk<#y?A@ff){{Au|iQfu+Og)P_lPcJE^Ffvuo{ zBai_=0~G~y4`Y7s{3*H*xOcux0XRRW3Z=Ma8j+z1TB!iN$TT7!RcPf>UIx93eLaw- zcA6SgoN}OXz5-fNU4Qtz!5Z|+w4#t}A=8Te4)(%Vb1?Rq82e=CdBF2adEPGv&&Rxv zAK6Fjrxg3VADz2?=?Q#U*85+x-m49Iy^Hbf+rI_<!~vNGLalosM%fy5V_7D|zF>D0 zRUO>~dEgLe-~{G+A>F@#@xO4I9$YM^`xh$!d{n>%9PynJ&f{E3CrmG@bi(JL6(yZC z<STiH_fpu{V5-c`d?BrfHK<9xqAIJrgjQIN#hQ1M_cw*<82e<5{a);k;qwH2X-Qlt zc%%=;-jUY%nLxiI@qEN^{MET@n<4*~#$F}`UJD?zY+xE-ThW98-6$~>wc}CuqIm6S zs@M?@zPBE0;C{MQl1X>L2bcyPRF>0&OK0dI;K8M{I7-0x9#pDA1!zTv--=cgJf*QU zr~zM5$Sy)wQOGOxX@zB4@DYh`fCjMtxt5EuPv1=!55r#|VLo(w<1sIIk7rLyJPjxa zHWiljU~7Nstkas~_Wnor_wsoD(j)dPGsM0CG%(7%3&jSSQFf#=YQ_gq*_Ih}A#o*L z$=pe`MaLiy<beiCG3FKY@G{1|>KuJlbslg*apd#!_>SRR9jMR=)5|ZXm9m%el?Lli zQ?w$lIgoQ}OELCESod=dL;k@Y@4!kbhmN#x^=RlykXIG^<ynqBg>RK&>~A}DYs2=u z|F|vhmyYYt`9Fnz0KP-*u$O_ob|~_jBTxCs<=#RjIKOuhbOiB`2@cWiQzz*DxgvT9 zKJW<R|ET&RJ*ttR66Z4D9)Ab;sv7jcAn2q%t*Fom)65HL1^lE^<|%x95nriCE09(0 zv%Dg&JCHqC4!wo3zfo|EuAG1mL<)Q)A^)6;M@-s^0O(4cX~$R#_)@@@$MM?WmC);! z!;go3H~&wp^{+|?e!bf2qkY$yz5}7lm~7L74on_^nsrzY){KI_cotne1YU5QdBIV- zTb@e~upWE`KEUJu_(~-`zIqAA%k=ft%Q#noD|}yqM>y8e2-Av!cK$=O!m^5>6P8aP zm$1yrdzahbWwphS_wr!#&VXI#&}ynkm<N78mQF756m>e5d!j}o^yb2c!$|wzUIuOB z^uPGxFaK?0FVl#Zar+OTBiJXe2P3f_EJh!s!>*Mtc)>(EyL%z#33PDTdm$I3(Y-S# z!3$1fo}Uvmz+?aQwQBnMdNn-}#~N|Y*Wy0kyH+LO@zpAII$@gm4QPeeAEryywdVds z9)HN1kY%Mge-wLr*npBZpiNO!7C!}b8Us+H*_HO9KK)ea$b?Rh*Lp*(=bn0+EaU%I z@Oyc@n>wP#-~_b!2i9u@_320x2kO$cG5uiQMjdO&1*bPpAzl-zj&8u3xes)Z4m!xk zx?V<)tIuOhD>1&8>B)^6z!iFOvxdIGk>SaWtI|Dj&e!<Ob*2#+B-(jZT9Nn(cuNiQ zmAdujA<H3EkTtMg+`oW52*&+R8OwOk6K8?fAKOUhq3<{i8K(&REpPEK<WaPv=wU`; z4V3KnLe484(!H%V`s%-m-#0b(@_j{~!E~7yj6hAMxz74@D2(@supg}+fxerHK7?#= zG*03N#mBHuz`Ag;6#U>U=JG``&v}f+_}5;eZ)&dtuF^L*aSnK@IDdk32|yn=nMPz_ z8o2`cV326!iOgHTM;<c>zQMc$Yfz0OUp#<}%KMeOXQ6N6aW8;9APatvDH!+tOCjIQ zgg;COba}&(JIRq^gUo15KOM352^r5&>lxy-rdT4*Uhb3sACJ&!JHd8z%O5f80f;Ra z2|5Tz{hP#THjoY6AtR5VvQ5w*?^%fb;##_znLs!44?|AOr2FXmhm~awY2hY#0ml6) z=;7%t1x?(#j_Vt^&({(>!SM-x$Mhi4i$n{8rwCf$bwnC>p8H=_VPA^(-8&DRWd&># zCD7~T!M`mNKCegCLAF~2nni8|>~C4$bv(j}Hjd~^6RmZneI5I1g6E;ViLUw@T!ZnC z4|;(8iZLf*&@Vqq@}WcrbDa%n_oRW)8KA!PN`ESa?x<q>Y`Sn@3FcWGT|KdjZh{u> zl=Aw(@?tU8<#MbMEGsZ?xCGvR8N30q0`mx7CjdOY-!P4cHG<a%e9!U&e#_(i^)<}% zD;ES^@$*;?y$4#mQ=Uz?A(PxVd05D)Rp9X#4lWnEj8o8W7p@us+0Fy99p*dsx#Rm| z+)W|h37b8SyJqEZeT{y940_`|;H$6Q*3VdLHRi})nGOU$Fz-y$9rS49NYpf*Hwg2} z6YD8>!p2FEr{~gz{je`5ucexdEp#2S!p*`YF-CXGv%nutV(bdVdhxK5_Y$Wuhq153 zm_Dkiz<Gr@=P~Aa{h$)MhYKax#}$H}^00q9LAQ!iL1Rf`{i)8_3V9B3E(Z|vxHFu} zAgAzLXW3QAb)f@kv4=7Ed^h16EBR&%`R<}mFHL{ei8bExKklmbK!9HRPcZ%wkP)AX zbpg5~o=3qJT`9uVh_;XGkJvY7K?{Ye0>Kv$f3O99hZqy4hswh%sp=Sfk1$?WPwW&j z<c*UD1+QTGs4YyUTSX}XYKvft!aZJ7uIC*94ef>ewp09X737XfNvptbmeSc>^WoPv z1Nw>ypudq~%ySl@7AoqZuMIS($+kUdBxp4dYnWm`$9(>*S1-+BXxsl+vYqN!{+e@+ zJ%_LHbm$E#g`9}>lX=1j*u_GiGnqBmkk$nDrTx=v;UhIf$XbP~5SP4eG{$K>=I2zb z7l=*T3BUB+h)>xU1=%c`E*@M0zP(hyMXV)EGyEO@=FF~e{Ld^wPp39cq>}X^kVS*2 z0CSvm73qk1+A)3rE%WM4)9myx=3T_v&N^<URpwoQJwClOtbR?~|M*|^9`G}4+Z8-v zG4uxyg-rppz;lgb5O@s6T6L!dLyT!nKwrTh4j~ubaqx&d)M3wG=0OE9-q?TpVU6~O zOc^ME$F&Ii&BB!dRJg*I^6{H|$m_iSV0t<_$C36-wxJEdmKg6oG|gU*g8Oy_{dEy? zyvmsW3pf?}8)=Sa9fvyRe_Z{RJ;3`V9|H}0AtxAWUBp~Np3-*kiIwPq?I)~b=6yu4 zMK|acdeCgtfS*6ah!%O6U>r?prLVc*8|#Dm(rSNmTJB>CJyaj)rHyGG_8wuF*CGA7 zQ?R*?cn;H8AoLVG-mJ4=`onvlfxjoiMmve+mfy0Ce?05edw_G*1|TL4yaBdGtqT~J z9{_?E^H?!mh;ak2;4x&K8`Fq7bQJTM`AH!3@Z$IQKS5X0So2yAzN3}S>k({PhJVO% zuRcb<=P!j1lKz+Nd<``0KogUhF5ruhfw4OcdGe;8vGzA&?(#gA;77j4>k7v85`LS{ zK1|4?6bZi@U)W)dg7vjIkIEmPKYHMg9{8gN{^)@}df<;9_@f8@&-VaHpNMA76=&k` z4DAY6xxvkRtM2-rqS<xVwc^@Bc_TtxE1&ppaoOxo^7lVT*UjYXTE(^efC$C4`~bzJ zjDhra+<Z-Xfc!Xk^NJ56FMYzrb*+5a%tBm8D6gd#C@-ZKD6r+b0%PUt6fdlMo%F)G z*GYX=|GKvB`2vAFm1_+k6rpm>2&r5Ogz8<ZpeGPiK~Er9|5_DYM!5d9D!SZ&`q#Cp z??(gbU+1ZPuR-;%Bh<ccuGU=SCTwu6_H%SVy=%4RqXT~KwOaR}!+z^)_3mxf?2l{p z0j$>_ul?HVdgJ$}pL<=8e%^Y?b@O`Pi{u0<Ev$d7QIEdsUt84s-lE?832$&+tMWFU z&-Jg><}Wy8{cE)j2ZK}VF!1>L*J=%}&!3oosp4aS>j;$_;keGLcU`M;jjvT3AQ-v| z!h-**AS{+36$AzURzc8$pQnOAxVWxEKwQ?nP_cd}U#M8&lrNB$2U5PkLi&H@3lx`f z3zh4e;(^KqQ}I9z`S*%0Q(j6KDgRx;qw9VyHL33B;s>f<tA6z}&$$qEPk=w*wPu?5 z`@4MD7A6D7vV90>1^5`idFt68M*beB|0_O>569nV0W1J))8o1-lN*7FxEBE6Tx{IE z9HY<w|3C5J_j2D3eEs!i<4^;9jk)$G$Lu?QUgoCz<xLO0FCTg7YdnWf!gD@Ho!A@p zT|Pf$rqwb<`{Q?4;(w;%e?tMD0M0Y=d&QeG-W=cV+@;kA>lt5vBaOd@S$CvWo?R(n zR8Q2y?S-7@$Uip62w*7Alcwm=_E9}(g=aUKHn0O?C^Y|S+xfF%Za1FAW5s!r^Z@@! zJ3RMMU%fUTZgKDV<=trmJEDG0cjRg`q{3)ps#w>DE^jxdYX=}sC0Ww#6u?nSx|M7} z*AJM}<peW2v%Zh`ZN_W^+7Qs4Ciid8wOnp={j~W;Je%9WwOl#p&u>jvA8UQ|?zAC2 z+FT7o|Lq;8M<*kV>1?bS@a;$UGWye_oB{OY<Uo2_Xid+G2h#IWYXRSu;=E)aJu4bO z-{kkFud=M@=3z@Zx6zdH7a7y8F+FLrRXZBo{fo*^-g`R|xbr;WSmxIOzxKoZ@P$d+ z7TZVmZugJPfjud2p)uawmu?*HPxmvd=~1o?J;}GFZwhS$JT0;T{(ur2;0-9Xp>Iz~ z@SQkH?fW`!0NqQqqKli&D1V_5#rbukKqIXmdbRmzC*H|pVGH;*K76me{dO~L<n1k) zX59fbJPoLHg&EcC?N4`7t?9uD8~Q5S7X1#$wWY^-5`2BqmKeBAPYTekg1UC8wT;_Y zWKEBA22$<eepJ53l#-_!P^eW08qnd>T;M(hz_F#js12>(d8ds_x7HWuIdr0|xh7N* zXF=BvTGQ=RJGz$+$grdPnSkSV^nl@n9X-sFfZO#Ix2v(Xal7QU@tAS@@~!D^+5oED z(wDO58PjaM)3M9v<qAG!hc^5yTKW`uQk}cDE{k;TOxfYR>Fh=;x_ZEdYE$g#)=>#= zAG1eW>@gOAG<&*>Hr-CMrOfrF6gR6Ut(?-G5|<gnr=b5!+xE3$?Cxg_q^ceLD0hJ| zg*$d8=25_%X^H2<tIp}yz^jwy+IL33gRjK(2hG^g)kJ%`e%L|4jU)%Uam1c(G9=rJ zcJ0A@*7~Ah-V?l+n!opFGSvBqCU~@?gG-I5_Lz+{ZWuFp+`h`T!`!yT*xJyg1WPdv z(``BuuNim-&uQjMFa7lI&}!6p3+;cW%`m1@Ypm$f4m+ye=Rh_49I0l%16?^F!PSEf zXp;k71MH9LgB-rEvk3J{@XlL*q5(#&X!G1&Xp1e~1ue?-crU{iyw`><ZbE(YS%x&m zOzU4AKY4F0p6v^G;asrn_(e;sEzkCkH=x3$mUMQrJzd=4K$mtp(xqLFba}S~ReKz% zYOf<zGoW3aH9mMHyc=uZXD!~QDPA3^D$$0RCYc`jcpcZLSNEkoWA(_hSKG&LzVUh( z{=XNX$&agU>wQbzx>5FgGdjKAj?QfvMCZ2xwhf{S+a<V|FbHiLM3p-nsS@ovw#@Pu z;|@fW|J(YLcPM?exkQhkNv6x{T?46LktszE?m`yrK1s*#M*%q2y}`##S3_%*nf6bq z(@dyv`9L}y=SUSBov0$-iOw)=a>95F62R>`$L#`S#tit?IY5DoXoq&|_o!;WJzYD5 zc?P(4z@E-*K;6|auEC)7_xIj>V>W)v@=}8jWP*fMp54K3EU9><1C_3GqEqXgsEi@j z3GI=fe1j8}bGziWp<O`^9hLov2mh+`o%4DNn&i1wvmaxeU_%AbX0*huD_OR0m4WB8 z%%{eS+<Lcp2lmnLk4`bByeJzgjB$eh%wPe<s|Qob8VO3*4o15M(<z2}ZSxt}{#W5G zw>vVphnQ=Z6mzX|hdq_8>yP^9Ml@Vs^BL9zUe{RmR(`w<G(G2m-)7E2p0Y)#VX+iG zgv*@a%O^oWj5BQxwIr85T4dCz6*(BUrI=ABq}sNk)lT%;N58&4?YdCpE0*UQ=Guh> zN6@b==%NpW*>obvy|F-?_YcaCr=G^aO##TMF&i~l5IdN?$OZYK7@U!B$(cf3deG~y zzt*6C?F`!@)^ad>oDg@raxnGR`(?EifQWc@^LO7wZYl>c*Ul*B+F8uCGx7G6zu1!2 z`|1+%5$ECeya!U!5c0-8JT%c5x#R5-6X62?ED265bfHCleO_4eW7-fs%3H!dpUyPI ztX+dP%DBDg$hAN_ec7BghMJ1Grkrm)*8r#E22oLrH65I2NbWtpyo+ZA$oTvAXw!OJ z-*%|4X^L8lgD7<_>Ma7&=egqCg+6}o?HA%?rt<}IWxI$m8*TUVYEZRl`7X`%>P4vw zZIyFPNs~O+6!VRFNzn>BN}tK~f3^Po)*G+${!Of9?2kCxt}`8-Vu@Pes1rT|{%tcQ zi1vDUO++X7SY^(4MICq-+C9aV{_@tV<<o85;$2FK;5<ML=UM^N;|kCvXp;FzF`zKU z0edAgnl_*#wf*RwrNEzc0?oV)H3Js8cBACU{V8dR3nfh*BH+lhA>`2O#q?m_r48aq zoG5LMV$3jZF##6z(Yxw7pzs>CG2}LGp4eYGZqi(H5p#{_8`GoUAu4UlTRI3iS}kb) zARW^Ey!j?P^JB)}S6_4Da<}f3GR20DOc{#(t2-T<>?Yvzj~cH>{FgWXOp_c8DLHHi z#tgt?rWm(lvxd-N)O(*j#Dx0lw4oO7tJ9y(mn|rM4A*T(o5P)Hv~724_gQl>$69{y z4tZI2LhabW4ceBo2y5jWOVknUMh4Ozhik?&{xep3^hAAmd*I=Pd>%e@WV$!)7&nwK z!PWW=KPLn2&na$%J=*CeAZhAQj2FWYI>LZ)<94aGZTCb+TIyp-TgKQTXAI^QYMQ2E z%zAffyk?WG<|m?Us(j?cLe#{VZAC2Kn6z!VAGkDU{87hj(u!d{kh30n^XCNsMu^(# zF#&@F?hg8$Nmt`@GVQEE3p@tWkuY!MG8|4x(-=H~w+GrWOgV0cF?I||XrEf!&@N@$ zFlGlP4{p#m+&?n-TXj|P5#}WssOgc0y!wk=x{-dHk9OgAoZGb-{Fwrm4n<zI+0Ll> zIvRE8$594q0PhX+BOfd1KO#n<FM`o7)CE_x?dY69IyyUmj?DC<O(8>Q=R|jmn>X6# zRoAwub!|JOpvn4e`{-SD+LCeSqxPpCD*4D!&}8bY!J-EBd?y|1(OT4=7X07WP<y~! z$FAW0PN-cv9`!4wx~3;k%k;$JNhIpqE}9_Kw2d5#wvC~*`61-j-v~9E>gsm1Y0;df z47Px8uGF@aS$>o<)0dKGNc1S$rW`Z0ZHfok>vd4}w<_*@{$+~~B${M;1f)y@Z=7O5 zVFNo;hnDY8!*2xsT>oUERY&x{J!*5J2KwsRQXMu$ea0NrWz1eajj~beSJZAr%~rpG zy_<Y*s7pW0ztL*rc5E(aa!!yKv!k>8iQATpwyhuO)a2Tzul<FRk4SS3_)j*YVAFR0 zZvNIA)-wKlL|npGDbuXz<SN7>%XJG;S4`AtM?J!mYq(Y=>dT>R$D$|GTi`$3M~#7g z2MyFFo`~_9Kp_r&N$b-V^xivfQRg<F(qyzPEkbHrs)8oXbhH|M&RAO$!AGQVV_I~k z{bNkXqo>A0yjKgL{P-Jc9@sXj59O^KkNR22o5M9xHlr>MYH3OJrlX`9<Z%mW*;t=O z--m~Z`yN)tsNXxCW)62~^mo?1y3lb!kEpYZxwc`PXM-_#8#USwPWKah<QUWAEMGbr z=0xj#^m$L8i+8E*VNg%kKhhPo=okBmdJbi%?I7x=%5_q?b}rY=MLn?v!A;k|<GRMl zin_-3COsR)pW7J~I7EdeCzG9_y4>Mpf?8e*J|b$&hl4MM+0bk|9Wv2qF$>QSw2!0s zxOMIN-U!q7-)GKs6ty4Ea-Bca`;%&0Aa5nt+d>^2t_Qkl?!+eXniuFU)y+fwXDfs5 zP5#c$wy&VcJk(6(nupn_gBla!K_9;N7vdV9yJrTg@R3QBxd6OwQeTlP@11v=wUO~w zJ;HZ;Yg|BYDq4qpbX>Da&fU+o%D7J1LDVHf{{2&^@ADVbe8B&zLAy4etJS(S@7Y!D zd)41haC0E8^~ZJF3dMZGTtiLJ=D^?9xQBAC<*l3ro-zgTqQhu~haPHpeSQefQR{oT z4IDQ;*0R$-vrvQa?7mf^W(wC&sZ`{ZmTKroImLVTXo%Zb4`-?V9>$Jq*Ko}wRh;GX z*U`U;OJ*wSQYmWNiMh6zf(G@cW6NiuZCsCxYtn^N?(zws{ecAi>rYs>y36=Bc7&hp zwzYn}Q8Q#J@ZX`#vn=u{BNwKq34)xs&I2seaQm!f3p$ywPSj!GnhOV3EKvJi#+{EI zw*90!aB^KHG1pLo7WH(bI$TnnYUUwDap9B|<wZ-}^~kCF7klvkjrP6V7WO&EJa~E( z`+#%%VyFgr{6rqmT*Ur!9(zUp`Vqri1b!=LPocx>m(jVDJ<7Im4V-yn8?SY3)=QTv zP+vx>ou{ai$2B3iZiuKkR5#a7ZI3~Y=t*>7k|pPncw*MJt=gVf#<~8{*RZYkG{}UU zXY|t1t;kE3DRME39L$P5brGRssWtK=Ds!7E+9vYbB5!Hg;T>ddsHZjtM!Gswfjsu2 zmJjmai#lM+yynP##P#14bL}j6M=5F>aQ)f|R-ON?$aPj9)h4f@i`X3PZiu-&4*7ME zFXU7%Vm3J*U&^~xeKLcx_ib0?5mw}v7HyOA0E_(dd8riO<}BLKQ>PQ1J-SciWEJ%U zGLf58QL{#(Nkv|F%(V;238;0k3^h21Q}|$A8m6bQrb$Gb_##+e|IhF}&s^o(hf3oo zqn{H+u1hKJp(1Y`=fC7UhMZ>%Z4-HN<y?fwKX|<`i&}s7DRoDF{Bs$H6nQ44T(vSy ziah8dFER4XqW(%1a!#Jzw_4<yjCSuuUIv<(99Po>nlHS~Yl6Q?yTTa6N0r7+L_W6z z$bnNLa&U?KD4eqmIdbk_KrSWDk0WyRNp0i2p69YsX+U37x|p3R<^EM^o5&H)`FlAR zIOkqP%>b?^bMC+z)E)DqXg34$Las?W<Qse;k}vs^^;(DpIJ#hn0s176YSOldd?SyL zQ;PG&$T@|i98-$iW1MG9*)~N!LeBl8$YUz!EEPEfk=K}W-f|xFv&i$AiP~+m2lXJt zlI;Ga{oY8EkbhTu#-#q;{+T!(I?&zo5!ZSYIciQLp1le=4ivc!IIjV6<jA>k9wDcN z$Za6<XQ{T0$Luun*%W~<WFT+ZHsmLrOuIs@X-wbFKVco62^?O?CmX@>rMJ8dHTod_ z=0c>qF{MR%A}%vp#OpppZ1&fP>F3z}r?+lMz_~s|?glB(i)a_}Lve1N`^eF7yYx8X z^mkDCwpny!t}}%@_d-lh`%-TstwwdTFGcPZf52mk{s}?6QEjA~F&%)ur4;e%myfJL zo{%IFm;UgQ6pzla=p2{M?Yn=TbI6=P{PAwYq%J|6aR}lM?T`-xcpJB?@iS`2wu#@o z&zou%^C#pk80K%%A$7b}x1XXtd(+kl)|7-jdCu|xIu$z(xj90S&ny&qt;S&Q<41>P zLWer0KSiP4V=TKM7OUN1#L+lK{7l<iQ#k$7`=VBblxt*}pK-f1)G@s>!d&O+Xp64@ z7;T~RBVtIOV!W!r2T~Bz7tQfQ%-eqH`~I8$4}My3P=Zhe2<Ic5nl$+Mj6H*KtOZf> zF%PZgBR4+0Su^20WuZ7~G;1b&p`eZVfxnlJ;M{nO$Z9Q)@XV;?YxxLj&*SU*N8A$k zaf|@)Cqn%FmB;$;k*{TZ<YRrj<@U(!M7u?M<@Sdw`h{==qN2ZQ75#^Xv-2SLs|6n^ z0`6&i<WI9^?5D-x4ERs^X^FTw#8~?nYPcf4b}aOL(*+o6jPW(naPcy3tMhxuJo@N< z`Y!UG`@q(k><@eEc#C#4&!IC#57D6*_yIBS`CQ!N@Ax<P?Gebq-u%DluK&o#Q@>+N z<S<(lV5IqcwtYw1JVF=yW<&VOn9!*ergSdOjLvWFOBXhq)42_1RJIB}Fi|FyHr-JC zmj5TfNb5QHVx+H8>lVKulCN>D%|K(#Zx*?9fi28{ikF&E<@SE?F|whDu;V|<wTIt` zJ$x(d;8S4--w^=L*+1r9`ar7QX-Or^`v@PAC|4cesrdx4lfl3dGvSwixX&;)X;TMu zpu~y#RJ_cLs`uEyr^1mQWDSB}ixcny<PV~+3kCtNLGaT76gkpUhGIwfu{h#;d-%iH z!S3B(^mXE-UNi;%QHaw#_DlQH$4Em9x}_`2+;oH=$hpk};Wyy~Uk?}ft^iIr14juS zW)G%^IfLQ10>~XqkAOGh{#f8H;r|3}czkk@@O!D+*`IRe8^f-u!}))1U~TE}b9#~Y z`-n|_5a-*2@*>TJJ^n_LE7cwyLbr|qQis4F#TESsNOy&g0{}SRV>mA1F5~|&+nE@D zCGI&+qVKQ4hoE4w8GM^`xt`?xKtqv_?xkpoJS~Qr*Vp^#QhuZ*T}*JMD+h<twZm?7 zEy<0pGaPZF8_7cj+)Notr3ntSaaM0y1{&BCWlm)~*`G$j{SM=vp@O>-|G{(vcJ`tr z<^o4QBaN#s8|w(j8_@ZuFByt4jpz#>2UohhcNkUe^MLK%gQ^b<qZ)>T!{|z)J5?QU zBOkl=&?_~xpT75(Ka**XRy55Q_NNtAbUnpY>UX*C(eJm@T<GdSNBA+Ai?If+b6vhB z=M3{$o(t)-`oK=^Lg%-8(uJMF=_13f;Z(WXQ^2J?p1|9KLR=f_a`1et4h`Y67VpqR zkFHd-#Zl^e^gH+c&Ez4%N96=;|5N&R;Fxxv*9|@*rrt>F>Hcwsl)n`I74cqlW{bCg zvs=CC>^5&Yx7|y?`2;WISs4Z${j1pn@iHaOfBMsF<nP##D)$YgtB2e{H}2@ep;R7g zO9v;Qwk>#1W8;i*T@nTUc4Dp-WMtG8i1kIj7hfs^oZbkC_oec9A1dGELlv8SD0{X0 zuk!sE+OJ(5-RvKRF+#2x(0e5KBlB?84rG}T`3RmLngIQBH0QJQ2aW+$wALTFq5$js zkyC~NI^bA8Iu$2@kM*mMuWBFi`5S+F9eNZuiEegz0zVJ<lMJFmuww^so<Wth5No>M zEZBiFP_r|8i7(|X4?=x8z=|N`Bo3tfm4Q@%oJuE=Z>?}O@}e;A0LI<P^q2de99=wE zqML1?8^D=O?vyppf@axv0v&32$$jXnuW>ZquNS3bzjk6#5M@Q9_TA!<z!k7~1dSPF zLLVyZQ5vv4&GE8S;2%gy^F5TbRlkp3bj^0&qV?WtbaNUspTER`;sbiJAMp{!kN0(` zfBZaYf(dfg`as`23U(?9G9yR9Z=`Wu_8d>t$zC#oa+Z#uw_r1P5ia$=`!mLNyo+I@ zbW^y}osPht0)7?GST^%9XxDALMSDsM>qi+2f+<7R?ZZwO<tN!W>p!2byTR_685vB+ zqk^eTOO{0I=4eyr`W?QY*~2a9SQK*RsL)LT@-NABQ?SAtwoG#x-?u%n{OD((F=UQi z7w|lLVFNq?8<}MP3x!Wg=O*84)>Rwr34y&VMA$l;!b!gGXawG{NI^G(cBH<~T{ePp zmjwx$&jZcpE%QY!-Ttt5bS7_oO=n-!G>wAYH4}E&oEX&HguQ`nM8{#PWZRbfeAT0+ zZYMe%9s*ktY+<k$WkgOO-~J}_{yUA^OnbF!L&w5L3c5*zZFX7UAc~*hCi<TH|HLBH z_*Kw+)}lc8`q<JU=dQ46KqfcPn6=bRmrl$d2HU|b*zI7mTRWGwOdr_*xA*Y=nO=j? zj_KH|hpE_sB>QAK@Qn&^AulT<>fJ?~j5}%3cqiEHKr870PfJ7al|Lz;YtW%B9h?z> zaTr}k^9zEI)7F8O4DHUc%}mB`nOhIkCm#;mKsXg`jKpyv&GB=^`tT;T`lva*4V$8~ zxdH4~Qz##}tJ)md#*@2hCgrNwRC8iv+^12N!nV4=uW_5YsSbQpBF8J~J{@x)b><*i zGDLwN#&?k`eD9(>VK<C|t!*i6ZA&O-VgStxLeANZu+_vzQ87Tqy%6{p#-SEp?0ncv zQHK|{j{J21wwDV0=Sa3-6?^cqQNxt|rb;V(eqhdMlIi|<)Hv9a9WlP$6yvKkbgsRQ zsP)M;pUZcyLjCuZqW*hXLJVvd%VE0!Y+FXBfO`&XO3oHW)Ux^e^bu-Sn(B6@t+U5S zxGVZz;2tOA9wBhgg1w1tQ1x-+>n&kH65X>sd)Xw+RXfa&F3?qKxcKX9cb{O{fli=S zMfr|ps8bjVd&GJ=vv(bANo!$mS%Vr@Yp8tJYQQS$-M!(Q=J8KjIz#mR@ipPJcIrq< zge|iK_T1ul2C47;?06@v&WHYt8o<X_%vAP$?ux14FZ~67;rw7Se@>a&7qvB_kaK+# zYz+!XMDBgq`_3IeKJEi?l(>9ugXhcdTQxP97KZqsW_TU_oA=bAoVZBQ_e?h#Yvwhu z4_fwu%*FjK`hEe{&`?Sa?E|}v#Gmne_;F|*+Z<$snlDpCtw*kvUYU}BIzZcT&9DtN z$*r_v+L%W1Ytj4zD%r7$I%&3P@cjVep31&wx(OOQQ0+Hsrv(Fl(EL`^8QT;E-=!eh z=&#S~TQcLuAGT4?1`Xy$4@17)EuuaZ+Y72t&-*g!lwD2*9NSG>qh_f+8~-Y<7l#G| z|1GGk+R)eeO{{&V_pDRW&FNihC}OlXy$fE|S@TOZ+)KAaqTcaf$OoO-p3Ctr;={RO zv3LG989o|kb}xb52Q{X%lb~DYI`fABiLh}Tq>3XugpC;gq{LIZ%@<UOI(rvVb`aN- zR{f68-+{gN+~F;#!3;RGS)v`#4cCEQJ$;Oz8$t7X*I@j@=+Gon$XA-rRpl#25`1uJ zG<Z&aOaO8N?}ObURRO4joX54WQ$!7*ZXMPA)<Oe^1NTJQy0~%Lbhm|{Jn+ACY$t4X zJ3%*~9oV6^Ma>X&a}jjIc%Rv`68^33#A_MoEsAkc{dgK{eHvh_^(-;e0``c-$bFfL zS_NE}^#tmaphhcdG;*y@8w*qV6V`yPz|+ps4E47U0QaLbe6U@EzH6`1hRP0Y7kyuy zc>pyJ_6s``)6V5I&<^OP3UpJMyahGx!cZ&NlzpYYVZDGVZt^*=#nY@iiCQt$Y1@(S zIR`ZZxvm@6jpVwJxfI~;N(0P#3)_mo9rYF%|M=(#_^!OE#Ib{x29+E}9#ECO=lWYR z-CPlLgPP&M_1xa&qBg_S0gy|fOOf%Ze~iHX67wc=nR`$8F9srS=waBsiWRk|N{}zO zn9_D_p+lS2!FI>FgX?g=4cw1xjTQKHZm&%hX^C}xFKQ%bz~04mN>MXg&<^M(_lT$y zcXF{OEp^xBIh)2f)W=Kpn)h+o&)<miXZbM%&rC)>+%ni2IFGedx8ecUt-w95y>_4L zX2EvHxQ`9+p&nhk(8b&|34iW;*s<ikza?t8W+Ojt7IN;Ui`tF_EBq0!rq6XsuQfHU zs`&9a>q7zzwI9X>07v9i<=o0#`{gm$>!{&;`-*xlsNEvfT##@-usN2ll;nx_a4j%V zJ5JR7l4}PRp@s<Tr6nh*8ujM#A?wBY88U7USf^4Sj~89DJ_J1H>hfWFqJCA?(T(V{ z64W6<ered!L>&sQIiajCp{P3o+#iYhMKbQF8Nq$eb!E`cwZ)mpLArvn;s3nUy$9=F zZonqk@kO{a^&O8j_5sIv&K#O*h5hg(<g-mgpOphY<iLiV?5W6&&w25Y@0@dA!xkv= zUMp~aRC7V}{oRU_RFj@SC7_wa$(A$~&tzQ+>wB7d|Et`O;5mbN&AcZXzKq9a48%To z4C*|@A)k7-$hZCV)eDM#MU986sQUt(9wF~K=lth-AeBk0;g3I(Qc!1l(GXqMqdsNX zn4kSBIK0Ylm_ITP=XGYTV>j9wY>IleHi#4OBF+hWW@oslOTe`W`25s{i6ZxG`fO{| zYwbg`@jv{(SoatI+CF;`zPuLlex79t<gEL4e7}w~&#^nPO+RM1fdD?|d;A^$hWFQ; z*Y5YWZLw^|@+0p7f$un_eF5iukN2+pn-{hBW#1uMKU^@?XTqGq_9(SQp6yZL0Jh6m z0N4(t0bsim%s0aB1hbCvn1^$idxV{dj}ZV?=+!^s)^GZWz|$IgPaf{8;!}%jRea@k z$nBEbDYskn4gN>jFLFOA`i*T&ngC0{Z((O*8p9kI2D{7#e-o`!fxWdJ1)6C6h$F)z zzK8F?mbKh}OL~MZ|1-QdHo!#d`x&+!DK-H1nR$kEYIPqvA8$_QH$m^brZ@7*8&a&l zF3mu^O@N8!_uz3GUsd;wE^6@Mx|fg{=8wR8SHd<`7}E#wq*nA8ww0&F_J}jKqvxmW z=-V>DDO&*BU<T67L;a{^Wgqzd=^$=H`$x7d@v|Dd-~~6B&tMOJGQz0~<t^%i*mA@? z=Q+Z*;{ZE~iVda20Wr@Gh<65@a-eTZ?FBpoZV%E2Af~Q2ML2cl{d_U^;|pG?{K~hN zmM-*+ch(^8=i(N`OJ@$E2MRmGqg<zYHWJu(U>{-o&J(up05@UdfqjRMY>TSiV?}Wo z7p#Z3nP(_p)W`|r=x3~Xd&d}k#DgJbH`Q6#5F{IjGi)Hv!hR#!KnBC!0sD<?1A+ZR z9X|=TN4bt7hIt3#-~CK9@A>v_)4GvYH<N1JYL=|^?Lil|Ar|n6EA;F`gl$H$&#;}q zm98Cerjafk=#!5mozNGEfi%}^MI&9?(e?!<bS2qQVPleT6E>zSC+NbhX)SCrSl5di zqX%B2!UuIm+#C7~_KPctZqTCx*#07HFhhaUP;wifshs*~M}zY>poi~o^cii7=uNlM z>iX|a`d~Vb@tlWuU_M2w_5sUnK_=S&$e7m``erw}yvIXfUtt@|Fsg>0y!wDUm2GwY zS-N-q&Ml~Liyh*1*}mlpJDf9O-OMP6`G$&J3TxQrrS9Dk$Hlgj;dDM>I9=GmHWb!> z1E2@5-0eXX+uZ2m4;t6qHP#n=ua9mk(SO&I(0_o+9fN4O2li6>8u8o*LdS2U{e1@P z4`mxYsbZ5i><nxlVLOQzo!tr$dhX${kqoCj5w_5Ky}I6)W1Qvwt2yY7euW?R0t@gc zt?&8W;8Px)3nLFY_0sh|up_|!5yv(V9|}htUB@p!rfym-X~K|Rz|TwI<~6WOgK=-F z{jEQFk17@YcL{xXY1c5&m_1Fm#$H@s%Z>YC_3$1-9x7bTb^^9>_|rtxW~l$n06SfQ z-(2`O*8i@t>o%?4qZQ-&O8tkv<No8ZJ+s-J)_TKd4r__8k=E&b<IJE>_lG_=5c=Fe zN{#eZ)7Qw)?$WlUz;9!y!wcWvP3sfdJ_9kN8+~9y0<irk-WxU~X>7|k4X1;Xd&7TB zs}!=`<K$_mK@k-QeeDS7Ye&#@cgqIP=eU&xzE%Rilh6ft_Un!qf`;=?^V8;(iJE{# zYyDtn0j%?bT<nXn1t`Y0biEg3(Ecn}K0;pWACDowJ?l3!6gCL!UY*q5uhX^_B_N(h z#xHAe5bP&Gv}eXJ3UM^1em%5EN23+FnCei<0xvogK8*V6Y7pBSn!~;_#jQ8&K+@Ps zW9$okj16c|`v=VZAJb<$K|eTIK-SVw>V&ut!;TuH)An<k?P&uYa)^jilJUz}I1;*9 z36A4<JZgl%O~%j8u%T^faS&>OVQeMvr_8yw;{9m<<5c)w7sk$2=ws*7nu$Kp!_GpS z0PByT^NyKDnTRcl4sxN$fWdSG@hB&v$3ss)4lyfZpd$`}9vixB(SO0@ZKbc)e@s8N z`W^8<()eaa2Vl=JkjMWKe8f&4oMMgmlW3(5xn%QVg$`5FU#7*(Ag#|^DWCZ+bW3X| z_yE5dO8jz`PnG&_$t2n}J)lACu5<71O8hbwV(iA63HtLj)Y!Dv*8uw9Wze~<Ma;z- z#9XX~u6Pw<6ISAW4EZ@&Dc>#61xHhT%3d2z<~_R5+t9}|e#^(g&b5y9@UzL<Ks`QW zNig`%(#ezs{EjaQq19e`ybs)n^~iI&jSloUGwFOH{L~XSL$?|a{n$q6_7D>SAN4*x zbQ-*W`J^CXkn!Q;pWk=`c^#%eA0JKoA`x@+XDQ~W%_kotRw5kZJCE{#TUJy6g@OOD zJjpc{0*%}MH9OjyE<$g5iS<fHcR?Sv6W6G<2|cRofWB()G1l!O@S8oNVO-5T#M+$R ziT*?1rLGFcI^CRR_zh9$df|r|ABh;f!I*pPzvn#{@P>|0bIWqnYd(JvwTIx7Tn!yk zHFS4XnFrxBet^QqjZl04hKMlu!tbPDHz&3G+y{G>%vS5a;dcF~6!=s?SIRol(v5SW zAJF5rb4<ASu%2K<@Af}sFY$*iF$uaN$>;l89%2xnquaZBv3Sq>@BT&j=vSsgCkOm= zpl`whlrYSGfU^HCLZ2x2--RUj=^x&TK3k8PCqp5Zw)?KWJP5gc`TW5>=p5?Z-GrWn zb#k@P3*AJ$^OEGf^ht}4C4ck++21~uhT08KzPqnsFRDbmhv>iK=)X+#AFiv?Q9m(l zAI5Yi>;W@qo)css$kmFbD-SGx`Ri+*TNhv|YOLNlQ$Tm2SGf!Q)gAaPr|sT~I;@i3 z?G|)~wF>+!Obp3bPnRxb9~E&A*9CrQbPc+;tBU@shCcZm>NKzOHDN!Aa`q8dzF6gi z<sd&3?MJ&O*`n6aUg#;{%M1TwsRlKCs9Dzo9h>a;UJD<7YYTI#hTs033dAY^zgy60 z*5Q{8pZ*NGm=ufH+Wv(2fIId3UFG%S6mp;*^4vdK8(;?Ad>r0?Mygo|y$;vRRrm4c zddT(h<C=EtyIul3a!{Xo1+DjoZre!f4$JoS-`Ci+=m*r}Sm2D@ORGkqO&Rbxua;^t zqW0c5sKY1K+C!a1)MDd$t&gFX;+l$F)BC~2GU)!25SuuL=DYM{T}6ra&#aLr{P;nW zJTF%HBED!-7<6F!g&ygf+o%P2Tf#%C4_QNxuUEl8z8v+(cfiIsiB@<Sqn=Frf3RH5 zwAENUU*#UpO^nBuK$8xCorzfgji~*Yy2yu$Vke=#(0o7y6|SE^sj!{L1)9<fyRO8) z^@mK${rD>Hc$MEUpJZN*zT5<#=hLV${RlNK{=oph?nitNd#retCuP3&D$k`n$sL&8 zRiSIpfZm~bEmRSCP){&KKz+c_>}P)D|I1T6rxwrTXY)IhuCaJG>nV6++7s|g^%Urv zHj+(UGYx$Nzi*B21mGOJJr!&082$}j@v}a>4~GqHb#Nc8pCTMP(Y{c9)Q&Qvvzz+S zIY2>-skj%8?~tS6U!b)euS)0G^Ek0yk^K};$0FZUmMc9k8%EDhyVJLT@5<fj`wBPu z{)`*^4+qnkjs0Mo7e1WNeS2whE{-NY;I9)#nQH$ld8P?`+1%;TNiTX_Fq|G24yUh+ zJn2cXCw)`mLEn^m(9=^M^o-vNc$n*om}O%cjsIocZeuS%8}0Dye`djF>Q<UB-96z; z_p*HHezp(YKH*K9=l7*K!Fse|o+)K-v8M+o@m{?9YsI@C=X+2t{OTA#A49D{s_)>v zIrquoX?^HQl0V%z7C<*s1E@9)knT@5ecCG58~mR*e;@fv?9JQK_Q-y8H`|LI=6OS3 zF&r`ECd~7mtB!}*XAbR(eWf2&Cyu0=LnG+QVL;LdIvzW;!L#Le$j9y-T2R>@H@bV= z7khCJS`42t-otPkIHr80iS|Epqa5hMuF-UH&uFULJBlvt1MD9~=l6^t-41m!r2NeK z#}TmeiFTvyoWEpbZ>^u0c3IEL`NdAfj=(;6ES=dtmbT1tp<x4%AJNMU*P|#GzG$z% z*3jos^&NiDXV)m&d}+5A^vTc<!8QiIk)ANxm`Y+NAU0?MtqXNglVN>q^r&L%IBNY# zLt8LEpZCPJsEwJ8J*?dB%lk&sjxk2uN2!3CBZx=NUp1Ku)`XHlC-t$kHR_BQ(uw45 z)uX{Uv}@ghvQXbkZui-QF?4PRLkJz4-JhRd1-<ik>2qBv7d|HG(UX++V$PxCJ-~>v zV<u7l>dDwwOrnKBj%01vncBB*NtSx;DScTmEeNtDldc-%Y5|{;)xn5`8BeD-jf4M- z1M6(y->vc8armSa!G9zHei^3SJJL`~BiaQY5V2QTiM_yzF!<-d*GTvqO+jq?6!;BH zrnKm>YI{X~chUMuh_ji9IN?EfzQ%W$S65T#4xuu{fR@28gkxFRpQQxx@Sz@#^jV7! zNw-~FiVqtp?URJR%51E$Gx44oXiFV!wQcpW^4&?(ths-xd3<+{Lk>s8p0aGua{ZaT z>*>(4Icl-+=6YQzYwdh;G&7*S@RNw0Hd6TgO!unu)3G(umHcGz-fggv@c1&%i5S!c zdx+RN88mqZt(`MTZ9Lb_98ZhJ*Tu@d4cl&3Y$O$LT}Hh+NjCJ&v&M*a7p$8}5ui`O z+fkPb@yP!ySUVnZiOI0{BK{GwdgivZ%I6z)?;_qkeT0|t9@FNDc*N^sPg92Y;ojIg zRB&uNWbrJl*_dDdl-CBdW9xF*W3OZ<N^zz|C+G@dO4<J0t6LY^y(*fn<fM>|d0j3D zd)U=4!`6H$74_kH@1oeZu-%@0sd#?}9K?s;13US5$?%7~Q;{!ZG?r~AjSPS-9I_+J zwUD_Z!zRKX<!v(1?M9Wyk4UmC_5oLN(C(}xAyX$!cVPbZZJ^$lYB~DY_ic0^?SJL1 z8ZBf;j&poibxt9}O7d$>aW3^S)TgR~<H8=ycKBN&RueI(uu~sj>`SBY3|^yT+*Oa* zyG&&Mox0FN)LCU2jO#87Sx_OT-a(A$SE%p)5Vl?+KSOT5S(HX8^IXLG!t|p0%=+it z4(_WM#L%DF6A8OvIeq)!E<L|@o1WdhMbB>EMBJcMAN(F_!=B#8`>LLzPnyEJ{yTWR z;XN?ZRfJ;~+8;KE3J}MBc5f7XDWd3P>}1*(<|y(yj^zG8&I*4MP0jkxdD%7Bl;iaj z{gwe4tr~lf?-}@<?=f%Z_2Wg)fmsJOE)CeXnm4P3r6>>9qX<Y$46pi;f6xER&){eA zGx^yphq3bpAAhbK#<7L0O9bp<UJPEw{0yL3CEe2+%D;IPJcaKjc;O7YPKfg~f-c?? zd0z+6&7}U&-S!3j7|<+y$NUn{AKcV4BZN<f#?o=-?U2vF0y2RMY!pN2n_^dbTH->_ zN}XXd84UZ46YLfa@RjaEV-a@*dRW5$TmQMxlP;Ty_g3y4O!rQBLEafo4{|&qGkMTg zdBY%Mxzpo(H^jRPr6)zwd#)wg(oDOKyw_i>`aF)y8e`s`&TfGp|FJ--g^Y76&7W$H zcu|mZ7t-t8lAJ6wXl77PN?VV-o!NEosX5?CoV!eJJNGO1Mf!YO$TT64X+}e)8AVr< zf+=KZ<M~9iT7N*XGy6i0@j+X>p-b$??Y<6u^qZhd_Zc>w=`7@n3%e&mj+ub<d^}{B zaWsEaKQ+FJHYu+;)}nZuhiK3BBp;dyUoX)0P~f>|*Tey+!5T*8Tc^>Q$*$DjpaV_! z=|^XGgo5Wcp1+Rem|W<<<@W5GV$Nf}6VIzjU*L^(cn);&qlCY_j7P#8Kk^vRQ1@rw zr!ytYb3=P3&_%SRa^E=UAzio+1+C_-7%%jFfsS?Uv%wmdy?Q1cT@p&xMqS}I_BQgB zzCk0M%qV%uD2hNntc;Z*s4qK(&Lm8RUfP?V_nhgzXnmN__qY!<Cr5K5S{pVBxi>iP z#$u7{hJ6YP;m4VSTquR>=TeL2QqHjE$OUtJ#U#Yw&VV0$0Jrx$*w(M)t$}|b>%xQ{ zXA5O-UQX@4{6gscCV9CCeIe@#*`JjC8F}57`((?EQB(jQ*NpiBziP-bd-u;mtPIwX zYUpXI;NLYp*jM>``@SZW5Y0ZN?~;zT2BpVF)8>d!@jdI(O14RUt2-y!vHf5dXnWXf zhpxgOi~Y4&-w{1sTJw3`%-tPNlLPD4<A`xVLKif1q&H3Wb4Ol`&G0u}L(^?_ggt_3 z5dC;9XYEwsf5kED`!=j5tuNcqoQWY=m$PWl084?-`|rL>C--j^>*ARdti8xH!uq<j z=pY`88~iMuckGie&Y}bS*>(#*q^gpWRFsy4wTsuoGKycmh~9-?;KBH{3f&Fsy*S52 zCY?RF2J@}G@G0kK$sf#<<ue_0v0f(8(>pgLoe1LczlI*-d_fK+ZrcQ12=p_T;O`>d za|(Wbn`ov3`v+?-k)I<UrI<~v<>M_nQt|eMSl_PE_m3XXw-4{p^ZR$O=G8*yf%nv2 z!<t%7C*x)SFXTlBUy5jiU&+tqw)5Pc?WjvfmyL(4vJJjBM~Q1?R|21u#iMDKJ>(Sh zIrAm?|6X(yV}x<X|Lg%&1HM-PpYLHFBKLj$=lt;6Gpsw$uzvLv5Ftti)c;lE*Zf=g z|M(gFEMBiZ1ZV<&&bq}u#^{$7*xO&8WYzAk43GscgJ;EXd;=cW=trVYjkU0=|Epx3 z89m4x2%9^@0D72d1v^h~3U}&+y)E<x_0~N8U&KM>97aw@-Vc9U?nU2Kc+!J>SM2kL z(qGTI(%;TvT`F;;!!r@@W2BL-`V7c5>kff$KQ10ePfEsO{~kiFiX5yhVUM;o|B_Z= z&;J$n_}`X!(&1Ug#CUK+*e55#sq4S)WKE|B1+(d4!A!cFJC%6<jIY(8chAr9?9ucn zKLBm%@-OZW@S#<?F<x{#2eS0xc{JX;AEj;xp#|gZ)&7^C^-=S8=&QmodQ>zO`D~m( zTUs%YC#x^+kD}|xH&X8&l6|Oa`!<xeF-&d$Vc)kKWo?;2ck?3Y-pK`6$0o3hetBZQ zc7LrtmOy8r$7VlPjW0f>#8va?YQ{GB%!ZPug9VNAa-bcHC(saUV@lsRoocgI(e?Cb znq=Aj`%sMUjoiajp1g}v;#Ug&`JNREY4>ux13t!A;Nw~Yzi3YfNvCbD-<__Z=6ubu z^~CRH{I6y0gCF8ax_dg8(spc6wmWCfW^%Pbp1-N%=uqr3ao^w75jx~Vs)S8}@#k?U zNmwZS-@byL^3c{e@jFxK&5tHX&w3O7^z0iO;xmlW6XH<QJ`;A6Fy<F4*mq))yUD+w zT&n~RsG;gp1=OueC(1auo8nh25zq2;aUy;;>&+jWM_!ilT+~4}gxuP;E7JgYe_RIk z-`_vF5597TYR;a9JY7o<p;vaaMSl943-Bj|4<vZl*VislM$C9#gE)`2_^{sse17t> z<7Uy{o<61@zI{r6fA)m__RV8@a{o4c|M&r(4Ssd+24$`er2u2i-1>ZgpMf<acB!A$ zzpJwL(sz&U&`&@9KtKKcZ}jc`Yji1NJ1zF^%X^P?_5WAKgOA)Fl5S4BYN~aYzcB>& j(XL`TWm;GLEuTMwQ&=7xt|A~VGra60|BnBM|M&j^&~%xO literal 0 HcmV?d00001 diff --git a/lmsreport.ini b/lmsreport.ini new file mode 100644 index 0000000..152728e --- /dev/null +++ b/lmsreport.ini @@ -0,0 +1,7 @@ +[DATA] +host=10.120.7.20 +port=7079 +database=lms +[PARAMS] +port=6543 +log=D:\PROJECTS\LAZARUS\LMS\out\server.log \ No newline at end of file diff --git a/lmsreport.lpi b/lmsreport.lpi new file mode 100644 index 0000000..012abcb --- /dev/null +++ b/lmsreport.lpi @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="UTF-8"?> +<CONFIG> + <ProjectOptions> + <Version Value="12"/> + <PathDelim Value="\"/> + <General> + <SessionStorage Value="InProjectDir"/> + <Title Value="lmsreport"/> + <Scaled Value="True"/> + <ResourceType Value="res"/> + <UseXPManifest Value="True"/> + <XPManifest> + <DpiAware Value="True"/> + </XPManifest> + <Icon Value="0"/> + </General> + <BuildModes> + <Item Name="Default" Default="True"/> + </BuildModes> + <PublishOptions> + <Version Value="2"/> + <UseFileFilters Value="True"/> + </PublishOptions> + <RunParams> + <FormatVersion Value="2"/> + </RunParams> + <RequiredPackages> + <Item> + <PackageName Value="Abbrevia"/> + </Item> + <Item> + <PackageName Value="frxe_lazarus"/> + </Item> + <Item> + <PackageName Value="fr_lazarus"/> + </Item> + <Item> + <PackageName Value="dcpcrypt"/> + </Item> + <Item> + <PackageName Value="nnzdata"/> + </Item> + <Item> + <PackageName Value="lnetbase"/> + </Item> + <Item> + <PackageName Value="LCL"/> + </Item> + </RequiredPackages> + <Units> + <Unit> + <Filename Value="lmsreport.lpr"/> + <IsPartOfProject Value="True"/> + </Unit> + <Unit> + <Filename Value="maintcpserver.pas"/> + <IsPartOfProject Value="True"/> + <ComponentName Value="CGIServerGUI"/> + <HasResources Value="True"/> + <ResourceBaseClass Value="Form"/> + <UnitName Value="MainTcpServer"/> + </Unit> + <Unit> + <Filename Value="tcpthreadhelper.pas"/> + <IsPartOfProject Value="True"/> + </Unit> + <Unit> + <Filename Value="connectionsdmunit.pas"/> + <IsPartOfProject Value="True"/> + <ComponentName Value="ConnectionsDM"/> + <HasResources Value="True"/> + <ResourceBaseClass Value="DataModule"/> + <UnitName Value="ConnectionsDmUnit"/> + </Unit> + <Unit> + <Filename Value="reportdmunit.pas"/> + <IsPartOfProject Value="True"/> + <ComponentName Value="ReportDM"/> + <HasResources Value="True"/> + <ResourceBaseClass Value="DataModule"/> + <UnitName Value="reportDMUnit"/> + </Unit> + <Unit> + <Filename Value="cgidm.pas"/> + <IsPartOfProject Value="True"/> + <ComponentName Value="NIDBDM"/> + <HasResources Value="True"/> + <ResourceBaseClass Value="DataModule"/> + <UnitName Value="cgiDM"/> + </Unit> + <Unit> + <Filename Value="xpaccessunit.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="xpAccessUnit"/> + </Unit> + <Unit> + <Filename Value="exttypes.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="extTypes"/> + </Unit> + <Unit> + <Filename Value="tcpserver.pas"/> + <IsPartOfProject Value="True"/> + </Unit> + <Unit> + <Filename Value="tcpclient.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="tcpClient"/> + </Unit> + <Unit> + <Filename Value="cgireport.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="cgiReport"/> + </Unit> + <Unit> + <Filename Value="cgicommand.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="cgiCommand"/> + </Unit> + <Unit> + <Filename Value="fr_utils.pas"/> + <IsPartOfProject Value="True"/> + </Unit> + <Unit> + <Filename Value="baseconnection.pas"/> + <IsPartOfProject Value="True"/> + </Unit> + </Units> + </ProjectOptions> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <Target> + <Filename Value="lmsreport"/> + </Target> + <SearchPaths> + <IncludeFiles Value="$(ProjOutDir)"/> + <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)"/> + </SearchPaths> + <Linking> + <Options> + <Win32> + <GraphicApplication Value="True"/> + </Win32> + </Options> + </Linking> + </CompilerOptions> + <Debugging> + <Exceptions> + <Item> + <Name Value="EAbort"/> + </Item> + <Item> + <Name Value="ECodetoolError"/> + </Item> + <Item> + <Name Value="EFOpenError"/> + </Item> + </Exceptions> + </Debugging> +</CONFIG> diff --git a/lmsreport.lpr b/lmsreport.lpr new file mode 100644 index 0000000..50d0e95 --- /dev/null +++ b/lmsreport.lpr @@ -0,0 +1,38 @@ +program lmsreport; + +{$mode objfpc}{$H+} + +uses + {$IFDEF UNIX} + cthreads, + {$ENDIF} + {$IFDEF HASAMIGA} + athreads, + {$ENDIF} + Interfaces, // this includes the LCL widgetset + sysutils,Forms, abbrevia, lnetbase, MainTcpServer, tcpthreadhelper, reportDMUnit, + ConnectionsDmUnit, cgiDM, xpAccessUnit, extTypes, tcpserver, tcpClient, +cgiReport, cgiCommand, fr_utils, baseconnection + { you can add units after this }; + +{$R *.res} + +begin + RequireDerivedFormResource:=True; + Application.Scaled:=True; + Application.Initialize; + if paramstr(1)='console' then + begin + Application.CreateForm(TCGIServerGUI, CGIServerGUI); + Application.Run; + end + else + begin + Application.CreateForm(TConnectionsDM,ConnectionsDM); + ConnectionsDM.start; + while ConnectionsDM.running do + sleep(1000); + + end; +end. + diff --git a/maintcpserver.lfm b/maintcpserver.lfm new file mode 100644 index 0000000..8e4c06d --- /dev/null +++ b/maintcpserver.lfm @@ -0,0 +1,146 @@ +object CGIServerGUI: TCGIServerGUI + Left = 333 + Height = 309 + Top = 224 + Width = 870 + Caption = 'Сервер отчетов LMS' + ClientHeight = 309 + ClientWidth = 870 + OnCreate = FormCreate + OnDestroy = FormDestroy + LCLVersion = '2.2.4.0' + object Panel1: TPanel + Left = 0 + Height = 50 + Top = 0 + Width = 870 + Align = alTop + Caption = 'остановлен' + ClientHeight = 50 + ClientWidth = 870 + TabOrder = 0 + object SendButton: TButton + Left = 96 + Height = 25 + Top = 14 + Width = 75 + Caption = 'Запрос' + OnClick = SendButtonClick + TabOrder = 0 + end + object StartButton: TButton + Left = 8 + Height = 25 + Top = 14 + Width = 75 + Caption = 'Запуск' + OnClick = StartButtonClick + TabOrder = 1 + end + end + object GroupBox1: TGroupBox + Left = 0 + Height = 259 + Top = 50 + Width = 368 + Align = alLeft + Caption = 'Запрос' + ClientHeight = 239 + ClientWidth = 364 + TabOrder = 1 + object Keys: TMemo + Left = 0 + Height = 216 + Top = 23 + Width = 364 + Align = alClient + Lines.Strings = ( + 'user=nnz' + 'password=Sochi-2020' + ) + TabOrder = 0 + end + object edtRequest: TComboBox + Left = 0 + Height = 23 + Top = 0 + Width = 364 + Align = alTop + ItemHeight = 15 + ItemIndex = 3 + Items.Strings = ( + 'version' + 'reports' + 'arguments' + 'login' + 'test' + 'logout' + 'option_values' + 'report' + 'status' + 'result' + ) + TabOrder = 1 + Text = 'login' + end + end + object GroupBox2: TGroupBox + Left = 373 + Height = 259 + Top = 50 + Width = 497 + Align = alClient + Caption = 'Ответ' + ClientHeight = 239 + ClientWidth = 493 + TabOrder = 2 + object edtAnswer: TEdit + Left = 0 + Height = 23 + Top = 25 + Width = 493 + Align = alTop + OnDblClick = edtAnswerDblClick + TabOrder = 0 + end + object retValues: TMemo + Left = 0 + Height = 88 + Top = 71 + Width = 493 + Align = alClient + TabOrder = 1 + end + object intValues: TListBox + Left = 0 + Height = 80 + Top = 159 + Width = 493 + Align = alBottom + ItemHeight = 0 + TabOrder = 2 + end + object edtQValue: TEdit + Left = 0 + Height = 23 + Top = 48 + Width = 493 + Align = alTop + TabOrder = 3 + end + object StatusPanel: TPanel + Left = 0 + Height = 25 + Top = 0 + Width = 493 + Align = alTop + TabOrder = 4 + end + end + object Splitter1: TSplitter + Left = 368 + Height = 259 + Top = 50 + Width = 5 + end +end diff --git a/maintcpserver.pas b/maintcpserver.pas new file mode 100644 index 0000000..72ca0f7 --- /dev/null +++ b/maintcpserver.pas @@ -0,0 +1,182 @@ +unit MainTcpServer; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, + tcpClient,tcpServer, tcpthreadhelper, + ConnectionsDmUnit, syncobjs, extTypes; + +type + + { TClientThread } + + + { TCGIServerGUI } + + TCGIServerGUI = class(TForm) + edtAnswer: TEdit; + edtQValue: TEdit; + edtRequest: TComboBox; + GroupBox1: TGroupBox; + GroupBox2: TGroupBox; + intValues: TListBox; + StatusPanel: TPanel; + retValues: TMemo; + Keys: TMemo; + SendButton: TButton; + Panel1: TPanel; + Splitter1: TSplitter; + StartButton: TButton; + procedure edtAnswerDblClick(Sender: TObject); + procedure SendButtonClick(Sender: TObject); + procedure StartButtonClick(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + private + + Server: TConnectionsDM; + Client: TClientMainThread; + cmdDone: boolean; + started: boolean; + procedure LogQuery(const qtype: integer; const command: string; const aKeys: TStrings; const code: DWORD; const Param: QWORD; const data: TParamArray); + function onAnswer(Sender: TMainThread; const mode: byte; + const Code:DWORD; const QValue: QWORD; const Answer: string; const Values: TStrings; const iValues: TParamArray; const Data: TStream): boolean; + public + + end; + + { TConnectionThread } + + +var + CGIServerGUI: TCGIServerGUI; + +implementation + +{$R *.lfm} +uses + types,strUtils; + + +{ TCGIServerGUI } + +procedure TCGIServerGUI.FormCreate(Sender: TObject); +begin + Server := TConnectionsDM.Create(self); + ConnectionsDM := Server; + cmdDone := true; + started := false; + SendButton.Enabled := false; +end; + +procedure TCGIServerGUI.SendButtonClick(Sender: TObject); +begin + if not started then exit; + client := TClientMainThread.Create(edtRequest.Text,Keys.Lines,@Server.log,'localhost',6543,@onAnswer); + LogQuery(0,edtRequest.Text,Keys.Lines,0,0,[]); + cmdDone := false; + edtAnswer.Text := ''; + edtQValue.Text := ''; + StatusPanel.Caption := 'Ожидание'; + retValues.Clear; + client.Start; + +end; + +procedure TCGIServerGUI.edtAnswerDblClick(Sender: TObject); +begin + Keys.Lines.Add('='+edtanswer.text); +end; + +procedure TCGIServerGUI.StartButtonClick(Sender: TObject); +begin + + Server.Start; + started := true; + Panel1.Caption := 'запущен'; + SendButton.Enabled := true; +end; + +procedure TCGIServerGUI.FormDestroy(Sender: TObject); +begin + Server.Free; +end; + +procedure TCGIServerGUI.LogQuery(const qtype: integer; const command: string; + const aKeys: TStrings; const code: DWORD; const Param: QWORD; + const data: TParamArray); +var + f: textfile; + logfile: string; + i: integer; +begin + logfile := ExtractFilePath(paramstr(0))+'out/query.log'; + assignfile(f,logfile); + if fileexists(logfile) then + append(f) + else + rewrite(f); + case qType of + 0: writeln(f,DateTimeToStr(now()),#09,'REQUEST'); + 1: writeln(f,DateTimeToStr(now()),#09,'RESULT'); + end; + writeln(f,#09,command); + if qType=1 then + writeln(f,#09,format('code=%d, value=0x%x',[code,Param])); + if assigned(aKeys) then + for i := 0 to akeys.Count-1 do + writeln(f,#09,#09,aKeys[i]); + + closefile(f); +end; + + + +function TCGIServerGUI.onAnswer(Sender: TMainThread; const mode: byte; + const Code: DWORD; const QValue: QWORD; const Answer: string; + const Values: TStrings; const iValues: TParamArray; const Data: TStream + ): boolean; +var + i: integer; + fs: TFileStream; +begin + try + LogQuery(1,Answer,Values,code,QValue,iValues); + edtAnswer.Text := Answer; + case mode of + cmdAnswer: StatusPanel.Caption := format('OK(%d)',[code]); + cmdError: StatusPanel.Caption := format('ERROR(%d)',[code]); + end; + edtQValue.Text:=IntToHex(QValue,16); + if assigned(Values) then + retValues.Lines.Assign(Values) + else + retValues.Clear; + intValues.Clear; + for i := low(iValues) to high(iValues) do + intValues.AddItem(inttostr(ivalues[i]),TObject(PtrInt(iValues[i]))); + if Assigned(Data) then + begin + Data.seek(0,soFromBeginning); + fs := TFileStream.Create(Answer,fmCreate); + try + fs.CopyFrom(Data,Data.size); + finally + fs.free; + end; + end; + finally + Sender.Terminate; + cmdDone := true; + end; + +end; + + + + +end. + diff --git a/numberinwords.pas b/numberinwords.pas new file mode 100644 index 0000000..8acc46a --- /dev/null +++ b/numberinwords.pas @@ -0,0 +1,464 @@ +unit numberinwords; + +interface +function number999(number: integer; gender: integer; declension: integer): string; +function number999_ord(number: integer; gender: integer; declension: integer): string; +implementation +function number900(centi: integer; declension: integer): string; +begin + case centi of + 0:; + 1: case declension of + 0: Result := 'сто'; + 1: Result := 'ста'; + 2: Result := 'ста'; + 3: Result := 'сто'; + 4: Result := 'ста'; + 5: Result := 'ста'; + end; + 2: case declension of + 0: Result := 'двести'; + 1: Result := 'двухсот'; + 2: Result := 'двумстам'; + 3: Result := 'двести'; + 4: Result := 'двумястами'; + 5: Result := 'двухстах'; + end; + 3: case declension of + 0: Result := 'триста'; + 1: Result := 'трехсот'; + 2: Result := 'тремстам'; + 3: Result := 'триста'; + 4: Result := 'тремястами'; + 5: Result := 'трехстах'; + end; + 4: case declension of + 0: Result := 'четыреста'; + 1: Result := 'четырехсот'; + 2: Result := 'четыремстам'; + 3: Result := 'четыреста'; + 4: Result := 'четырьмястами'; + 5: Result := 'четырехстах'; + end; + 5: case declension of + 0: Result := 'пятьсот'; + 1: Result := 'пятисот'; + 2: Result := 'пятистам'; + 3: Result := 'пятьсот'; + 4: Result := 'пятьюстами'; + 5: Result := 'пятистах'; + end; + 6: case declension of + 0: Result := 'шестьсот'; + 1: Result := 'шестисот'; + 2: Result := 'шестистам'; + 3: Result := 'шестьсот'; + 4: Result := 'шестьюстами'; + 5: Result := 'шестистах'; + end; + 7: case declension of + 0: Result := 'семьсот'; + 1: Result := 'семисот'; + 2: Result := 'семистам'; + 3: Result := 'семьсот'; + 4: Result := 'семьюстами'; + 5: Result := 'семистах'; + end; + 8: case declension of + 0: Result := 'восемьсот'; + 1: Result := 'восьмисот'; + 2: Result := 'восьмистам'; + 3: Result := 'восемьсот'; + 4: Result := 'восемьюстами'; + 5: Result := 'восьмистах'; + end; + 9: case declension of + 0: Result := 'девятьсот'; + 1: Result := 'девятисот'; + 2: Result := 'девятистам'; + 3: Result := 'девятьсот'; + 4: Result := 'девятьюстами'; + 5: Result := 'девятистах'; + end; + end; +end; +function Adjective1(gender,declension: integer): string; +begin + case declension of + 0: case gender of + 0: result := 'ый'; + 1: result := 'ая'; + 2: result := 'ое'; + end; + 1: case gender of + 0: result := 'ого'; + 1: result := 'ой'; + 2: result := 'ого'; + end; + 2: case gender of + 0: result := 'ому'; + 1: result := 'ой'; + 2: result := 'ому'; + end; + 3: case gender of + 0: result := 'ый'; + 1: result := 'ую'; + 2: result := 'ое'; + end; + 4: case gender of + 0: result := 'ым'; + 1: result := 'ой'; + 2: result := 'ым'; + end; + 5: case gender of + 0: result := 'ом'; + 1: result := 'ой'; + 2: result := 'ом'; + end; + end; +end; +function Adjective2(gender,declension: integer): string; +begin + if (gender=0) and (declension in [0,3]) then result := 'ой' + else result := Adjective1(gender,declension); +end; +function Adjective3(gender,declension: integer): string; +begin + case declension of + 0: case gender of + 0: result := 'ий'; + 1: result := 'яя'; + 2: result := 'ье'; + end; + 1: case gender of + 0: result := 'ьего'; + 1: result := 'ей'; + 2: result := 'ьего'; + end; + 2: case gender of + 0: result := 'ьему'; + 1: result := 'ей'; + 2: result := 'ьему'; + end; + 3: case gender of + 0: result := 'ий'; + 1: result := 'ью'; + 2: result := 'ье'; + end; + 4: case gender of + 0: result := 'ьим'; + 1: result := 'ьей'; + 2: result := 'ьим'; + end; + 5: case gender of + 0: result := 'ьем'; + 1: result := 'ьей'; + 2: result := 'ьем'; + end; + end; +end; + +function number900_ord(centi: integer; gender: integer; declension: integer): string; +begin + case centi of + 0:; + 1: result := 'сот'+Adjective1( gender, declension); + 2: result := 'двухсот'+Adjective1( gender, declension); + 3: result := 'трехсот'+Adjective1( gender, declension); + 4: result := 'четырехсот'+Adjective1( gender, declension); + 5: result := 'пятисот'+Adjective1( gender, declension); + 6: result := 'шестисот'+Adjective1( gender, declension); + 7: result := 'семисот'+Adjective1( gender, declension); + 8: result := 'восьмисот'+Adjective1( gender, declension); + 9: result := 'девятисот'+Adjective1( gender, declension); + end; +end; + +function number90(decades: integer; declension: integer): string; +begin + case decades of + 0,1:; + 2: case declension of + 0: Result := 'двадцать'; + 1: Result := 'двадцати'; + 2: Result := 'двадцати'; + 3: Result := 'двадцать'; + 4: Result := 'двадцатью'; + 5: Result := 'двадцати'; + end; + 3: case declension of + 0: Result := 'тридцать'; + 1: Result := 'тридцати'; + 2: Result := 'тридцати'; + 3: Result := 'тридцать'; + 4: Result := 'тридцатью'; + 5: Result := 'тридцати'; + end; + 4: case declension of + 0: Result := 'сорок'; + 1: Result := 'сорока'; + 2: Result := 'сорока'; + 3: Result := 'сорок'; + 4: Result := 'сорока'; + 5: Result := 'сорока'; + end; + 5: case declension of + 0: Result := 'пятьдесят'; + 1: Result := 'пятидесяти'; + 2: Result := 'пятидесяти'; + 3: Result := 'пятьдесят'; + 4: Result := 'пятьюдесятью'; + 5: Result := 'пятидесяти'; + end; + 6: case declension of + 0: Result := 'шестьдесят'; + 1: Result := 'шестидесяти'; + 2: Result := 'шестидесяти'; + 3: Result := 'шестьдесят'; + 4: Result := 'шестьюдесятью'; + 5: Result := 'шестидесяти'; + end; + 7: case declension of + 0: Result := 'семьдесят'; + 1: Result := 'семидесяти'; + 2: Result := 'семидесяти'; + 3: Result := 'семьдесят'; + 4: Result := 'семьюдесятью'; + 5: Result := 'семидесяти'; + end; + 8: case declension of + 0: Result := 'восемьдесят'; + 1: Result := 'восьмидесяти'; + 2: Result := 'восьмидесяти'; + 3: Result := 'восемьдесят'; + 4: Result := 'восьмьюдесятью'; + 5: Result := 'восьмидесяти'; + end; + 9: case declension of + 0: Result := 'девяносто'; + 1: Result := 'девяноста'; + 2: Result := 'девяноста'; + 3: Result := 'девяносто'; + 4: Result := 'девяноста'; + 5: Result := 'девяноста'; + end; + end; +end; +function number90_ord(decades: integer; gender: integer; declension: integer): string; +begin + case decades of + 0,1:; + 2: Result := 'двадцат' + Adjective1(gender,declension); + 3: Result := 'тридцат' + Adjective1(gender,declension); + 4: Result := 'сороков' + Adjective2(gender,declension); + 5: Result := 'пятидесят' + Adjective1(gender,declension); + 6: Result := 'шестидесят' + Adjective1(gender,declension); + 7: Result := 'седидесят' + Adjective1(gender,declension); + 8: Result := 'восьмидесят' + Adjective1(gender,declension); + 9: Result := 'девяност' + Adjective1(gender,declension); + end; +end; +function number19(number: integer; declension: integer): string; +var + p1,p2: string; +begin + case declension of + 0: p2 := 'надцать'; + 1: p2 := 'надцати'; + 2: p2 := 'надцати'; + 3: p2 := 'надцать'; + 4: p2 := 'надцатью'; + 5: p2 := 'надцати'; + end; + case number of + 11: p1 := 'один'; + 12: p1 := 'две'; + 13: p1 := 'три'; + 14: p1 := 'четыр'; + 15: p1 := 'пят'; + 16: p1 := 'шест'; + 17: p1 := 'семь'; + 18: p1 := 'восемь'; + 19: p1 := 'девят'; + end; + Result := p1 + p2; +end; +function number19_ord(number: integer; gender: integer; declension: integer): string; +var + p1: string; +begin + case number of + 11: p1 := 'один'; + 12: p1 := 'две'; + 13: p1 := 'три'; + 14: p1 := 'четыр'; + 15: p1 := 'пят'; + 16: p1 := 'шест'; + 17: p1 := 'семь'; + 18: p1 := 'восемь'; + 19: p1 := 'девят'; + end; + Result := Result +'надцат'+Adjective1(gender,declension) +end; +function number9(number: integer; gender: integer; declension: integer): string; +begin + case number of + 0:; + 1: case declension of + 0: case gender of + 0: Result := 'один'; + 1: Result := 'одна'; + 2: Result := 'одно'; + end; + 1: case gender of + 0: Result := 'одного'; + 1: Result := 'одной'; + 2: Result := 'одного'; + end; + 2: case gender of + 0: Result := 'одному'; + 1: Result := 'одной'; + 2: Result := 'одному'; + end; + 3: case gender of + 0: Result := 'один'; + 1: Result := 'одну'; + 2: Result := 'одно'; + end; + 4: case gender of + 0: Result := 'одним'; + 1: Result := 'одной'; + 2: Result := 'одним'; + end; + 5: case gender of + 0: Result := 'одном'; + 1: Result := 'одной'; + 2: Result := 'одном'; + end; + end; + 2: case declension of + 0: case gender of + 0,2: Result := 'два'; + 1: Result := 'две'; + end; + 1: Result := 'двух'; + 2: Result := 'двум'; + 3: Result := 'двух'; + 4: Result := 'двумя'; + 5: Result := 'двух'; + end; + 3: case declension of + 0: Result := 'три'; + 1: Result := 'трех'; + 2: Result := 'трем'; + 3: Result := 'три'; + 4: Result := 'тремя'; + 5: Result := 'трех'; + end; + 4: case declension of + 0: Result := 'четыре'; + 1: Result := 'четырех'; + 2: Result := 'четырем'; + 3: Result := 'четыре'; + 4: Result := 'четырьмя'; + 5: Result := 'четырех'; + end; + 5: case declension of + 0: Result := 'пять'; + 1: Result := 'пяти'; + 2: Result := 'пяти'; + 3: Result := 'пять'; + 4: Result := 'пятью'; + 5: Result := 'пяти'; + end; + 6: case declension of + 0: Result := 'шесть'; + 1: Result := 'шести'; + 2: Result := 'шести'; + 3: Result := 'шесть'; + 4: Result := 'шестью'; + 5: Result := 'шести'; + end; + 7: case declension of + 0: Result := 'семь'; + 1: Result := 'семи'; + 2: Result := 'семи'; + 3: Result := 'семь'; + 4: Result := 'семью'; + 5: Result := 'семи'; + end; + 8: case declension of + 0: Result := 'восемь'; + 1: Result := 'восьми'; + 2: Result := 'восьми'; + 3: Result := 'восемь'; + 4: Result := 'восемью'; + 5: Result := 'восьми'; + end; + 9: case declension of + 0: Result := 'девять'; + 1: Result := 'девяти'; + 2: Result := 'девяти'; + 3: Result := 'девять'; + 4: Result := 'девятью'; + 5: Result := 'девяти'; + end; + + end; +end; +function number9_ord(number: integer; gender: integer; declension: integer): string; +begin + case number of + 1: result := 'перв' + Adjective1(gender,declension); + 2: result := 'втор' + Adjective2(gender,declension); + 3: result := 'трет' + Adjective3(gender,declension); + 4: result := 'четверт' + Adjective1(gender,declension); + 5: result := 'пят' + Adjective1(gender,declension); + 6: result := 'шест' + Adjective2(gender,declension); + 7: result := 'седьм' + Adjective2(gender,declension); + 8: result := 'восьм' + Adjective2(gender,declension); + 9: result := 'девят' + Adjective1(gender,declension); + end; +end; +function number999(number: integer; gender: integer; declension: integer): string; +begin + result := number900(number div 100, declension); + number := number mod 100; + case number of + 0..9: result := result + ' ' + number9(number,gender,declension); + 11..19: result := result + ' ' + number19(number,declension); + else begin + result := result +' '+ number90(number div 10,declension); + number := number mod 10; + result := result +' '+ number9(number,gender,declension); + end; + end; +end; + +function number999_ord(number: integer; gender: integer; declension: integer): string; +var + top,rem: integer; +begin + top := number div 100; + rem := number mod 100; + if rem = 0 then + begin + result := number900_ord(top,gender,declension); + exit; + end; + result := number900(top,0); + top := rem; + rem := top mod 10; + case top of + 1..9: result := result + ' ' + number9_ord(top,gender,declension); + 11..19: result := result + ' ' + number19_ord(top,gender,declension); + else begin + if rem = 0 then + result := result +' '+ number90_ord(top div 10,gender,declension) + else + result := result +' '+ number90(top div 10, 0) + ' ' + number9_ord(rem,gender,declension); + end; + end; + +end; +end. diff --git a/reportdmunit.lfm b/reportdmunit.lfm new file mode 100644 index 0000000..0965e17 --- /dev/null +++ b/reportdmunit.lfm @@ -0,0 +1,94 @@ +object ReportDM: TReportDM + OldCreateOrder = False + Height = 213 + HorizontalOffset = 694 + VerticalOffset = 317 + Width = 330 + object frxReport: TfrxReport + Version = '2023.1' + DotMatrixReport = False + EngineOptions.SilentMode = True + EngineOptions.NewSilentMode = simSilent + IniFile = '\Software\Fast Reports' + PreviewOptions.Buttons = [pbPrint, pbLoad, pbSave, pbExport, pbZoom, pbFind, pbOutline, pbPageSetup, pbTools, pbEdit, pbNavigator, pbExportQuick, pbCopy, pbSelection] + PreviewOptions.Zoom = 1 + PrintOptions.Printer = 'Default' + PrintOptions.PrintOnSheet = 0 + ReportOptions.CreateDate = 45166.790207419 + ReportOptions.LastChange = 45166.790207419 + ScriptLanguage = 'PascalScript' + ScriptText.Strings = ( + 'begin' + '' + 'end.' + ) + OnLoadTemplate = frxReportLoadTemplate + OnLoadDetailTemplate = frxReportLoadDetailTemplate + Left = 176 + Top = 24 + Datasets = <> + Variables = <> + Style = <> + end + object frxPDFExport1: TfrxPDFExport + UseFileCache = True + ShowProgress = True + OverwritePrompt = False + DataOnly = False + EmbedFontsIfProtected = False + InteractiveFormsFontSubset = 'A-Z,a-z,0-9,#43-#47 ' + OpenAfterExport = False + PrintOptimized = False + Outline = False + Background = False + HTMLTags = True + Quality = 95 + Author = 'FastReport' + Subject = 'FastReport PDF export' + Creator = 'FastReport' + ProtectionFlags = [ePrint, eModify, eCopy, eAnnot] + HideToolbar = False + HideMenubar = False + HideWindowUI = False + FitWindow = False + CenterWindow = False + PrintScaling = False + PdfA = False + PDFStandard = psNone + PDFVersion = pv17 + Left = 55 + Top = 97 + end + object frxODSExport1: TfrxODSExport + UseFileCache = True + ShowProgress = True + OverwritePrompt = False + DataOnly = False + PictureType = gpPNG + OpenAfterExport = False + Background = True + Creator = 'FastReport' + Language = 'en' + SuppressPageHeadersFooters = False + Left = 47 + Top = 24 + end + object frxODTExport1: TfrxODTExport + UseFileCache = True + ShowProgress = True + OverwritePrompt = False + DataOnly = False + PictureType = gpPNG + OpenAfterExport = False + Background = True + Creator = 'FastReport' + Language = 'en' + SuppressPageHeadersFooters = False + Left = 108 + Top = 61 + end + object AbUnZipper1: TAbUnZipper + Left = 180 + Top = 86 + end +end diff --git a/reportdmunit.pas b/reportdmunit.pas new file mode 100644 index 0000000..b8771cd --- /dev/null +++ b/reportdmunit.pas @@ -0,0 +1,693 @@ +unit reportDMUnit; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils, frxClass, frxExportPDF, frxExportODF, + xpMemParamManagerUnit, AbUnzper, frxDBSet, cgiDM,extTypes; + +type + TExportFileType = (ftPDF,ftRTF,ftXLS);//(ftPDF,ftMail,ftRTF,ftXLS,ftHTML); + { TReportDM } + TReportQuery=class; + + { TReportQuery } + + TReportQuery=class + private + fQueries: TList; + fOwner: TReportQuery; + fData: TNIDBDM; + function getQuery(index: integer): TReportQuery; + function getQueryCount: integer; + public + Name,SQL,LinkField,Description: string; + ID,ParentID: integer; + Data: TfrxDBDataset; + property Queries[index: integer]:TReportQuery read getQuery; + property QueryCount: integer read getQueryCount; + property MasterQuery: TReportQuery read fOwner; + constructor Create; + destructor Destroy; override; + procedure Clear; + procedure AddQuery(q: TReportQuery); + procedure RemoveQuery(q: TReportQuery); + function Find(QueryID: integer): TReportQuery; + end; + + TReportDM = class(TDataModule) + AbUnZipper1: TAbUnZipper; + frxODSExport1: TfrxODSExport; + frxODTExport1: TfrxODTExport; + frxPDFExport1: TfrxPDFExport; + frxReport: TfrxReport; + function frxReportLoadDetailTemplate(Report: TfrxReport; + const TemplateName: String; const AHyperlink: TfrxHyperlink): Boolean; + procedure frxReportLoadTemplate(Report: TfrxReport; + const TemplateName: String); + private + TempTableAlreadyCreated : Boolean; + ComponentContainer : TList; + MasterDataSets: TStringList; // Список Master-датасетов (TfrxDBDataset) - не забыть создать/удалить как ComponentContainer. Возможно, не нужен??? + DetailDataSets: TStringList; // Список Detail-датасетов. В Objects - TStringList (id query + Detail-датасет(TfrxDBDataset)) + ReportVariables: TxpMemParamManager; + ReportQueries: TReportQuery; + procedure CreateDBDataSet(Query:TReportQuery; EditReport: Boolean = False); + procedure CreateSignaturesDataSet(EditReport: Boolean = False); + procedure CreateLogosDataSet(EditReport: Boolean = False); + procedure BuildPodpis(AVariables : TxpMemParamManager); + // Возвращает имя Master-датасета по id Detail-а (пустую строку если датасет не имеет Master-а) + function GetMasterDSName(qryID: integer): string; + // Возвращает имя ключевого поля (для Master-датасета - ключевое поле, для Detail-датасета - поле внешнего ключа) + function GetLinkFieldName(qryID: integer): string; + // Возвращает строку для фильтрации Detail-датасета. Параметры: + // - detail_key_fields, master_key_fields - строки, описывающие поля связи master и detail - датасета, + // - Link_type - тип связи ("ID" - по id, "BETWEEN" - по диапазону дат, "LIKE" - по подстроке и т.п. - сейчас пока только по id) + // - MasterDataSet - Master-датасет. + function GetFilterClause(detail_key_fields, master_key_fields, Link_type: string; MasterDataSet: TfrxDBDataset): string; + procedure PrepareBuildInfo(AVariables : TxpMemParamManager); + function GetVariable(AVariables: TxpMemParamManager; VarName: string; DefaultValue: string): string; + class function maskFRSpecial(value: string): string; + class function maskFRSpecialPreservingEOLs(value: string): string; + procedure frxReportPreview(Sender: TObject); + procedure LoadQueries; + procedure LoadDefaultVariables(AVariables : TxpMemParamManager); + procedure LoadLogos(AVariables : TxpMemParamManager); + procedure LoadVariables(AVariables, AParam : TxpMemParamManager); + procedure OnMasterRecord(Sender: TObject); + procedure LoadReportTemplate(); + procedure CopyReportVariables(AVariables, AParam: TxpMemParamManager); + public + RecordID: integer; + NidbData: TNIDBDM; + procedure ExportReport( ExportType: TExportFileType; Data: TStream; OnStage: TLogger); + end; + +var + ReportDM: TReportDM; + +implementation +uses + xpReportUtil, Variants,DB,lazUTF8, xpUtilUnit,nnz_data_components,frxCross, Graphics, fr_utils; +{$R *.lfm} + +{ TReportQuery } + +function TReportQuery.getQuery(index: integer): TReportQuery; +begin + result :=TReportQuery(fQueries[index]); +end; + +function TReportQuery.getQueryCount: integer; +begin + result := fQueries.Count; +end; + +constructor TReportQuery.Create; +begin + fQueries := TList.Create; + fOwner := nil; + ID := 0; + ParentID := 0; +end; + +destructor TReportQuery.Destroy; +begin + Clear; + fQueries.Free; + if assigned(Data) then + FreeAndNil(Data); + inherited Destroy; +end; + +procedure TReportQuery.Clear; +var + i: integer; +begin + for i := 0 to fQueries.Count-1 do + TReportQuery(fQueries[i]).Free; + fQueries.Clear; +end; + +procedure TReportQuery.AddQuery(q: TReportQuery); +var + p: TReportQuery; +begin + p := self; + while assigned(p) do + begin + if p=q then exit; + p := p.MasterQuery; + end; + if assigned(q.MasterQuery) then + q.MasterQuery.RemoveQuery(q); + fQueries.Add(q); + q.fOwner := self; +end; + +procedure TReportQuery.RemoveQuery(q: TReportQuery); +begin + fQueries.Remove(q); +end; + +function TReportQuery.Find(QueryID: integer): TReportQuery; +var + i: integer; +begin + result := nil; + if self.ID=QueryID then result := self + else + for i := 0 to QueryCount-1 do + begin + Result := Queries[i].Find(QueryID); + if assigned(Result) then + exit; + end; + +end; + +{ TReportDM } + +procedure TReportDM.frxReportLoadTemplate(Report: TfrxReport; + const TemplateName: String); +begin + NidbData.log(self,'LoadTemplate '+TemplateName); +end; + +function TReportDM.frxReportLoadDetailTemplate(Report: TfrxReport; + const TemplateName: String; const AHyperlink: TfrxHyperlink): Boolean; +begin + NidbData.log(self,'LoadDetailTemplate '+TemplateName); +end; + +procedure TReportDM.CreateDBDataSet(Query: TReportQuery; EditReport: Boolean); +var + i: integer; + DBQuery: TnnzQuery; + ds: TfrxDBDataset; +begin + if Query.ID>0 then + begin + NidbData.log(self,'CreateDBDataSet '+Query.Name); + ds := TfrxDBDataset.Create(Self); + Query.Data := ds; + ds.Tag := PtrInt(Query); + // Master/Detail + if Query.ParentID=0 then + begin // Если это мастер-датасет, добавить его в список и присвоить ему события + Query.Data.OnFirst := @OnMasterRecord; + Query.Data.OnNext := @OnMasterRecord; + end; + Query.Data.Name := Query.Name; + + + DBQuery := TnnzQuery.Create(Self); + DBQuery.Connection := NidbData.connection; + DBQuery.SQL.Text := Query.SQL; + try + DBQuery.Open; + except on E: Exception do + begin + NidbData.logError(self,e,Query.SQL); + raise Exception.Create(Format('%s::"%s"'#13#10' '#13#10'%s',[e.ClassName,e.Message,Query.SQL])); + end; + end; + Query.Data.DataSet := DBQuery; + + end; + for i := 0 to Query.QueryCount-1 do + begin + CreateDBDataSet(query.Queries[i],EditReport); + end; +end; + +procedure TReportDM.CreateSignaturesDataSet(EditReport: Boolean); +begin + +end; + +procedure TReportDM.CreateLogosDataSet(EditReport: Boolean); +begin + +end; + +procedure TReportDM.BuildPodpis(AVariables: TxpMemParamManager); +begin + +end; + +function TReportDM.GetMasterDSName(qryID: integer): string; +begin + +end; + +function TReportDM.GetLinkFieldName(qryID: integer): string; +begin + +end; + +function TReportDM.GetFilterClause(detail_key_fields, master_key_fields, + Link_type: string; MasterDataSet: TfrxDBDataset): string; +begin + +end; + +procedure TReportDM.PrepareBuildInfo(AVariables: TxpMemParamManager); +begin + +end; + +function TReportDM.GetVariable(AVariables: TxpMemParamManager; VarName: string; + DefaultValue: string): string; +begin + +end; + +class function TReportDM.maskFRSpecial(value: string): string; +var + i: integer; + isSpace: boolean; + uchar: string; +begin + isSpace := false; + result := ''; + value := UTF8Trim(value); + for i := 1 to UTF8Length(value) do + begin + uchar := UTF8Copy(value,i,1); + if uchar <= ' ' then // убиваем и заменяем одинарным пробелом все последовательности непечатаемых символов, включая перенос строки + begin + if not isSpace then + result := result + ' '; + + isSpace := true; + end + else + begin + if uchar = #39 then // одиночная кавычка, апостроф (') + result := result + #39#39 + else + result := result + uchar; + + isSpace := false; + end; + + end; + + result := #39 + result + #39; // 'result' +// result :=QuotedStr(''); +end; +class function TReportDM.maskFRSpecialPreservingEOLs(value: string): string; +var + i: integer; + isSpace, isEOL: boolean; + uchar: string; +begin + isSpace := false; + isEOL := false; + result := ''; + value := UTF8Trim(value); + for i := 1 to UTF8length(value) do + begin + uchar := UTF8Copy(value,i,1); + if inArray(uChar,[#13,#10],false) then // любые последовательности переноса строк, в т.ч. от макоси (#13) и линукса (#10), меняем одним #13#10 + begin + if not isEOL then + result := result + sFRBreak; + isEOL := true; + isSpace := false; + end + else if (uchar <= ' ') then // заменяем одинарным пробелом все последовательности пробелов и непечатаемых символов, КРОМЕ переносов строк + begin + if not isSpace then + result := result + ' '; + + isSpace := true; + isEOL := false; + end + else + begin + if uchar = #39 then // одиночная кавычка, апостроф (') + result := result + #39#39 + else + result := result + UTF8Copy(value,i,1); + + isSpace := false; + isEOL := false; + end; + + end; + + if (utf8pos(#13, result) = 0) then // в строке result нет символа CR; нужно забрать всё в одинарные кавычки, и FR их не покажет + result := #39 + result + #39; +// result := QuotedStr(''); +end; + +procedure TReportDM.frxReportPreview(Sender: TObject); +var + Report: TfrxReport; +begin + inherited; + try + Report := Sender as TfrxReport; + Report.PreviewForm.BringToFront; + except + end; +end; + +procedure TReportDM.LoadQueries; +var + SQL: string; + q: TReportquery; + i: integer; +begin + NidbData.log(self,'LoadQueries'); + SQL := format( + 'select q.xp_rpt_q_id,qp.xp_rpt_q_id as ParentID,q.Link_field, '+ + ' q.Name,'+ + ' q.Description, q.SQL '+ + 'FROM xp_report_query q '+ + ' left join xp_report_query qp ON q.Parent_q_id=qp.xp_rpt_q_id AND qp.xp_rpt_id=q.xp_rpt_id '+ + 'WHERE q.xp_rpt_id = %0:d '+ + 'ORDER BY qp.xp_rpt_q_id is not null, q.Name ', + [integer(RecordID)]); + with NidbData.GetData(SQL) do + try + while not eof do + begin + q := TReportQuery.Create; + ReportQueries.AddQuery(q); + q.Name:=fieldbyname('Name').AsString; + q.SQL:=fieldbyname('SQL').AsString; + q.LinkField:=fieldbyname('Link_field').AsString; + q.Description:=fieldbyname('Description').AsString; + q.ID:=fieldbyname('xp_rpt_q_id').AsInteger; + q.ParentID:=fieldbyname('ParentID').AsInteger; + + next; + end; + finally + free; + end; + for i := ReportQueries.QueryCount-1 downto 0 do + if ReportQueries.Queries[i].ParentID>0 then + begin + NidbData.log(self,'LoadQueries.'+ReportQueries.Queries[i].Name); + q := ReportQueries.Find(ReportQueries.Queries[i].ParentID); + if assigned(q) then + q.AddQuery(ReportQueries.Queries[i]); + end; + NidbData.log(self,'LoadQueries-OK'); +end; + +procedure TReportDM.LoadDefaultVariables(AVariables: TxpMemParamManager); +var + SQL: string; + l: TStrings; + i: integer; +begin + NidbData.log(self,'LoadDefaultVariables'); + + SQL := 'select name,value from options where name in (''GOU_Name'',''Dep_Name'')'; + with NidbData.GetData(sql) do + try + while not eof do + begin + AVariables[fieldbyname('name').asString] := fieldbyname('value').asString; + next; + end; + finally + free; + end; + +end; + +procedure TReportDM.LoadLogos(AVariables: TxpMemParamManager); +var + sql: string; + img: TJPEGImage; + p: TPicture; + s: TStream; + v: Variant; +begin + NidbData.log(self,'LoadLogos'); + SQL := 'select name,value from options where name in (''Dep_Logo'',''GOU_Logo'')'; + with NidbData.GetData(sql) do + try + while not eof do + begin + s := CreateBlobStream(FieldByName('value'),bmRead); + p:=TPicture.Create; + img := TJPEGImage.Create(); + try + img.LoadFromStream(s); + p.Graphic:=img; + // внутри вызывается Assign, поэтому img больше не нужен + finally + s.Free(); + img.Free; + end; + v := PtrInt(p); + AVariables[fieldbyname('name').asString] := PtrInt(p); + p := TPicture(PtrInt(v)); + + next; + end; + finally + free; + end; +end; + +procedure TReportDM.LoadVariables(AVariables, AParam: TxpMemParamManager); +var + sql: string; +begin + NidbData.log(self,'LoadVariables'); + sql := 'select name,value_string, value_int from tmp_report_variables where var_type=0'; + with NidbData.GetData(sql) do + try + while not eof do + begin + NidbData.log(self,fieldbyname('name').AsString); + if not fieldbyname('value_string').IsNull then + AVariables[fieldbyname('name').AsString] := fieldbyname('value_string').AsString + else if not fieldbyname('value_int').IsNull then + AVariables[fieldbyname('name').AsString] := fieldbyname('value_int').AsInteger; + Next; + end; + finally + free; + end; + NidbData.log(self,'LoadParams'); + sql := 'select name,value_string, value_int from tmp_report_variables where var_type=1'; + with NidbData.GetData(sql) do + try + while not eof do + begin + NidbData.log(self,fieldbyname('name').AsString); + if not fieldbyname('value_string').IsNull then + AParam[fieldbyname('name').AsString] := fieldbyname('value_string').AsString + else if not fieldbyname('value_int').IsNull then + AParam[fieldbyname('name').AsString] := fieldbyname('value_int').AsInteger; + Next; + end; + finally + free; + end; + // + + // + NidbData.log(self,'LoadVariables-OK'); +end; + +procedure TReportDM.OnMasterRecord(Sender: TObject); +var MasterDS_Index, i, idx: integer; + masterDS,detailDS: TfrxDBDataset; + master_key_field, detail_key_field, FilterClause: string; + DetailDSList: TStringList; + q: TReportQuery; + +begin + try + master_key_field := ''; + detail_key_field := ''; + FilterClause := ''; + masterDS := TfrxDBDataset(Sender); + q := TReportQuery(masterDS.Tag) ; + master_key_field := q.LinkField; + for i := 0 to q.QueryCount-1 do + begin + detailDS := q.Queries[i].Data; + detail_key_field := q.Queries[i].LinkField; + FilterClause := GetFilterClause(detail_key_field, master_key_field, 'ID', masterDS); + if FilterClause <> '' then + begin + detailDS.DataSet.Filter := FilterClause; + detailDS.DataSet.Filtered := True; + end; + end; + + except on e: Exception do + begin + NidbData.logError(self,e,'OnMasterRecord'); + raise; + end; + + end; +end; + +procedure TReportDM.LoadReportTemplate; +var + ReportStream : TMemoryStream; + BlobStream : TStream; + +begin + NidbData.log(self,'ExportReport.TemplateArh'); + ReportStream := TMemoryStream.Create; + try + with NidbData.GetData(format('select TemplateArh from xp_report where xp_rpt_id=%d',[RecordID])) do + try + BlobStream := CreateBlobStream(FieldByName('TemplateArh'), bmRead); + try + UnpackReport(BlobStream, ReportStream,AbUnZipper1); + finally + BlobStream.Free; + end; + + finally + free; + end; + if ReportStream.Size > 0 then + begin + ReportStream.Position := 0; + try + frxReport.LoadFromStream(ReportStream); + + except on e: Exception do + begin + NidbData.logError(self,e,'frxReport.LoadFromStream'); + raise; + end; + end; + end; + finally + ReportStream.Free; + end; // try +end; + +procedure TReportDM.CopyReportVariables(AVariables, AParam: TxpMemParamManager); +var + i: integer; + v: variant; +begin + NidbData.log(self,'CopyReportVariables'); + for I := Low(AVariables.Params) to High(AVariables.Params) do + begin + if VarIsStr(AVariables.Params[i][1]) then + v := maskFRSpecial(VarToStr(AVariables.Params[i][1])) + else + v:= AVariables.Params[i][1]; + frxReport.Variables[AVariables.Params[i][0]] := v; + end; + for I := Low(AParam.Params) to High(AParam.Params) do + begin + if VarIsStr(AParam.Params[i][1]) then + v := maskFRSpecialPreservingEOLs(VarToStr(AParam.Params[i][1])) + else + v := AParam.Params[i][1]; + frxReport.Variables[AParam.Params[i][0]] := v; + end; + +end; + + + +procedure TReportDM.ExportReport(ExportType: TExportFileType; Data: TStream; + OnStage: TLogger); +var + I : Integer; + flt : TfrxCustomExportFilter; + v : Variant; + AVariables, AParam: TxpMemParamManager; +begin + frxReport.EngineOptions.EnableThreadSafe:=true; + NidbData.log(self,'ExportReport'); + ReportQueries := TReportQuery.Create; + AVariables := TxpMemParamManager.Create; + AParam := TxpMemParamManager.Create; + try + if assigned(OnStage) then + OnStage(self,'список запросов'); + LoadQueries; + LoadDefaultVariables(AVariables); + LoadLogos(AVariables); + LoadVariables(AVariables,AParam); + frxReport.EngineOptions.DestroyForms := False; + // Создаём источники данных + if assigned(OnStage) then + OnStage(self,'подготовка данных'); + + CreateDBDataSet(ReportQueries); + if assigned(OnStage) then + OnStage(self,'загрузка шаблона'); + + LoadReportTemplate; + CopyReportVariables(AVariables,AParam); + TxpFRFunctions.SetReport(NidbData,AVariables); + NidbData.log(self,'preparing'); + if assigned(OnStage) then + OnStage(self,'формирование отчета'); + + begin + try + frxReport.PrepareReport(False); + frxReport.OnPreview := @frxReportPreview; + + except on e: Exception do + begin + NidbData.logError(self,e,'frxReport.PrepareReport'); + raise; + end; + end; + + case ExportType of + ftPDF: flt := TfrxPDFExport.Create(self); + ftRTF: flt := TfrxODTExport.Create(self); + ftXLS: flt := TfrxODSExport.Create(self); + end; + try + if assigned(OnStage) then + OnStage(self,'выгрузка'); + + NidbData.log(self,'exporting'); + flt.ShowDialog := false; + flt.Stream := Data; + flt.FileName:=''; + flt.ShowProgress := false; + try + frxReport.Export(flt); + + except on e: Exception do + begin + NidbData.logError(self,e,'frxReport.Export'); + raise; + end; + end; + finally + flt.Free; + end; + end; + //FreeContainer; + finally + ReportQueries.Free; + AVariables.Free; + AParam.Free; + end; + NidbData.log(self,'Report complete'); +end; + + +end. + diff --git a/tcpclient.pas b/tcpclient.pas new file mode 100644 index 0000000..6a43057 --- /dev/null +++ b/tcpclient.pas @@ -0,0 +1,111 @@ +unit tcpClient; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils, tcpthreadhelper, extTypes; +type + { TClientMainThread } + TRequestComplete=function(Sender: TMainThread; const mode: byte; + const Code:DWORD; const QValue: QWORD; const Answer: string; const Values: TStrings; const iValues: TParamArray; const Data: TStream): boolean of object; + TClientMainThread=class(TMainThread) + private + fHost: string; + fData: TStream; + fFields: TStrings; + fCommand: string; + fOnComplete: TRequestComplete; + public + property Host: string read fHost; + property Command: string read fCommand write fCommand; + constructor Create(ACommand: string; AFields: TStrings; ALogger: TLogger;AHost: string; APort: integer; OnReceive:TRequestComplete); + destructor Destroy; override; + procedure execute; override; + procedure ProcessConnect(thread: TConnectionThread); override; + procedure ProcessAnswer(const mode: byte; const Code:DWORD; const QValue: QWORD; const Answer: string; const Values: TStrings; const iValues: TParamArray; const Data: TStream); + end; + + { TClientThread } + + TClientThread=class(TConnectionThread) + public + class function Role: string; override; + procedure ProcessMessage(const mode: byte; const Code:DWORD; const Param:QWord; const ACommand: string;const Values: TStrings; const intData: TParamArray; const Data: TStream); override; + end; + +implementation +constructor TClientMainThread.Create(ACommand: string; AFields: TStrings; + ALogger: TLogger; AHost: string; APort: integer; OnReceive: TRequestComplete); +begin + inherited Create(TClientThread,ALogger,APort); + FreeOnTerminate:=true; + fOnComplete:=onReceive; + Connect.OnConnect:=@doConnect; + fCommand := ACommand; + fFields := TStringList.Create; + if assigned(AFields) then + fFields.assign(AFields); + fHost := AHost; +end; + +destructor TClientMainThread.Destroy; +begin + log(self,'destroy'); + Connect.Disconnect(); + fFields.Free; + inherited Destroy; +end; + +procedure TClientMainThread.execute; +begin + log(self,'start main thread'); + Connect.Connect(Host,Port); + while not terminated do + begin + Connect.CallAction; + sleep(10); + end; + Connect.Disconnect(); + log(self,'terminated'); +end; + +procedure TClientMainThread.ProcessConnect(thread: TConnectionThread); +begin + thread.SendMessage(cmdRequest,0,0,self.Command,self.fFields); +end; + +procedure TClientMainThread.ProcessAnswer(const mode: byte; const Code: DWORD; + const QValue: QWORD; const Answer: string; const Values: TStrings; + const iValues: TParamArray; const Data: TStream); +begin + try + if assigned(fOnComplete) then + fOnComplete(self,mode,code,qValue,Answer,Values,iValues,Data); + + except on e:Exception do + begin + log(self,'!!ERROR ProcessAnswer '+e.message); + raise; + end; + end; +end; + +class function TClientThread.Role: string; +begin + result := 'CLIENT'; +end; + +procedure TClientThread.ProcessMessage(const mode: byte; const Code: DWORD; + const Param: QWord; const ACommand: string; const Values: TStrings; + const intData: TParamArray; const Data: TStream); +begin + log(format('ProcessMessage(%d) Param=%x, Answer=%s ',[code,Param,ACommand])); + terminate; + Owner.Terminate; + (Owner as TClientMainThread).ProcessAnswer(mode,code,Param,ACommand,Values,intData,Data); +end; + +end. + diff --git a/tcpserver.pas b/tcpserver.pas new file mode 100644 index 0000000..e712835 --- /dev/null +++ b/tcpserver.pas @@ -0,0 +1,134 @@ +unit tcpserver; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils, tcpthreadhelper, extTypes; +type + { TServerThread } + + TServerThread=class(TConnectionThread) + class function Role: string; override; + procedure ProcessMessage(const mode: byte; const Code:DWORD; const Param:QWord; const ACommand: string;const Values: TStrings; const intData: TParamArray; const Data: TStream); override; + end; + + { TServerMainThread } + TCommandReceived=function(Sender: TMainThread; + const CommandID:DWORD; const Param:QWord; const ACommand: string; const Fields: TStrings; const iParams: TParamArray; const Data: TStream; + out Code: DWORD; out RetValue: QWord; out Answer: string; out rValues: TStrings; out iValues: TParamArray; out ByteData: TStream ): boolean of object; + + TServerMainThread=class(TMainThread) + private + fOnReceive: TCommandReceived; + fOnIdle: TNotifyEvent; + function processReceive(const CommandID:DWORD; const Param:QWord; const ACommand: string; const Fields: TStrings; const iParams: TParamArray; const Data: TStream; + out Code: DWORD; out RetValue: QWord; out Answer: string; out rValues: TStrings; out iValues: TParamArray; out ByteData: TStream): boolean; + public + property OnIdle: TNotifyEvent read fOnIdle write fOnIdle; + procedure execute; override; + constructor Create( ALogger: TLogger; APort: integer; OnReceive:TCommandReceived); + end; + +implementation + { TServerMainThread } + +function TServerMainThread.processReceive(const CommandID: DWORD; + const Param: QWord; const ACommand: string; const Fields: TStrings; + const iParams: TParamArray; const Data: TStream; out Code: DWORD; out + RetValue: QWord; out Answer: string; out rValues: TStrings; out + iValues: TParamArray; out ByteData: TStream): boolean; +begin + log(self,'ProcessReceive '+ACommand); + if assigned(fOnReceive) then + result := fOnReceive(self,CommandID,Param,ACommand,Fields,IParams,Data,Code,RetValue,Answer,rValues,iValues,ByteData) + else + begin + log(self,'Processor not assigned'); + result := false; + Code := ErrorProcessor; + RetValue := 0; + Answer := 'Server error'; + rValues := nil; + setLength(iValues,0); + ByteData := nil; + end; +end; + +procedure TServerMainThread.execute; +var + n: integer; +begin + log(self,'start main thread'); + Connect.Listen(Port); + n := 0; + while not terminated do + begin + try + Connect.CallAction; + + except on e: Exception do + log(e, '!!ERROR '+e.message); + end; + sleep(10); + inc(n); + if n>100 then + begin + if Assigned(fOnIdle) then fOnIdle(self); + n :=0; + end; + inc(n); + end; +end; + +constructor TServerMainThread.Create(ALogger: TLogger; APort: integer; + OnReceive: TCommandReceived); +begin + inherited Create(TServerThread,ALogger,APort); + fOnReceive := OnReceive; + Connect.OnAccept:=@Accept; + //FreeOnTerminate:=true; +end; +{ TServerThread } + +class function TServerThread.Role: string; +begin + result := 'SERVER'; +end; + +procedure TServerThread.ProcessMessage(const mode: byte; const Code: DWORD; + const Param: QWord; const ACommand: string; const Values: TStrings; + const intData: TParamArray; const Data: TStream); +var + s: string; + Vals: TStrings; + B: TStream; + res: DWORD; + rVal: QWord; + iVals: TParamArray; + ok: boolean; +begin + log(format('ProcessMessage(%d) Param=%x, Command %s ',[code,Param,ACommand])); + try + ok := (Owner as TServerMainThread).ProcessReceive(Code,Param,ACommand,Values,IntData,Data,res,rVal,s,Vals,iVals,B); + try + if OK then + SendMessage(cmdAnswer,res,rVal, s,Vals,iVals,B) + else + SendMessage(cmdError,res,rVal,s,Vals); + finally + if Assigned(Vals) then Vals.Free; + if Assigned(B) then B.Free; + end; + + except on e:Exception do + begin + log('!!ERROR ProcessMessage '+e.message); + raise; + end; + end; +end; + +end. + diff --git a/tcpthreadhelper.pas b/tcpthreadhelper.pas new file mode 100644 index 0000000..626aba7 --- /dev/null +++ b/tcpthreadhelper.pas @@ -0,0 +1,1047 @@ +unit tcpthreadhelper; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils,lNet,lnetbase, syncobjs, extTypes; + +type + + TMainThread=class; + TConnectionThread=class; + + { TConnectionThread } + + TConnectionThread=class(TThread) + private + fSocket: TLSocket; + fCache: TRoundBuffer; + fOwner: TMainThread; + class function BufferToString(const Buffer: TBuffer; const len: integer): string; + class procedure AddToBuffer(const Value: byte; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: word; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: dword; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: QWord; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: string; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: TGUID; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: TBuffer; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: TStream; var Buffer: TBuffer; var pos: integer); overLoad; + class procedure AddToBuffer(const Value: TParamArray; var Buffer: TBuffer; var pos: integer); overLoad; + + class procedure ReadFromBuffer(var Value: byte; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(var Value: word; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(var Value: dword; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(var Value: QWord; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(var Value: string; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(var Value: TGUID; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(var Value: TBuffer; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(out Value: TStream; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(out Value: TStrings; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure ReadFromBuffer(out Value: TParamArray; const Buffer: TBuffer; var pos: integer); overLoad; + class procedure InitBuffer(buffSize: dword; out Buffer: TBuffer; out pos: integer); + public + ID: TGUID; + recNo: qword; + property Owner: TMainThread read fOwner; + property Socket:TLSocket read fSocket; + property Cache: TRoundBuffer read fCache; + procedure log(msg: string); + procedure ProcessMessage(const mode: byte;const Code:DWORD; const Param:QWord; const ACommand: string;const Values: TStrings; const intData: TParamArray; const Data: TStream); virtual; abstract; + class function Role: string; virtual; abstract; + procedure SendBuffer(const Buffer: TBuffer; Len: dword); + function ReceiveBuffer(var Buffer: TBuffer; out Len: dword): boolean; + + procedure SendHeader(packetType,state: byte; Code:DWORD; QP:QWORD;SP:string); + function ReceiveHeader(out packetType,state: byte;out Sender:TGUID; out num:QWORD; out Code:DWORD;out QP:QWORD;out SP:string): boolean; + + procedure SendData(part: byte; const Data: TStream); overload; + procedure SendData(part: byte; const Data: TBuffer); overload; + procedure SendData(part: byte; const Data: TStrings); overload; + procedure SendData(part: byte; const Data: TParamArray); overload; + + procedure SendMessage(const mode: byte; const Status: DWORD; const QParam: QWord; const sParam: string); overload; + procedure SendMessage(const mode: byte; const Status: DWORD; const QParam: QWord;const AValue: string; const AKeys: TStrings); overload; + procedure SendMessage(const mode: byte; const CommandID: DWORD;const QParam: QWord; const AValue: string; const AKeys: TStrings;const IntData: TParamArray; const AData: TStream); overload; + + + + function ReceiveMessage(out mode: byte;out Sender: TGUID; out rNum: QWord;out CommandID: DWORD; out QParam:QWord; out Value: string; out intData: TParamArray; out Keys: TStrings; out Data: TStream): boolean; virtual; + + constructor Create(Aowner: TMainThread; ASocket:TLSocket); + destructor Destroy; override; + procedure Execute;override; + procedure TerminatedSet; override; + end; + TConnectionThreadClass = class of TConnectionThread; + + { TMainThread } + + TMainThread=class(TThread) + private + fCon: TLTCP; + fPort: integer; + fclients: TList; + flogger: TLogger; + fThreadClass: TConnectionThreadClass; + function getThread(index: TLSocket): TConnectionThread; + procedure TerminateClients; + protected + procedure Log(Sender:TObject; msg: string); + public + property Port: integer read fPort; + property Connect: TLTCP read fCon; + property Client[index: TLSocket]: TConnectionThread read getThread; + procedure RemoveClient(clt:TConnectionThread); + procedure dataReady(aSocket: TLSocket); + procedure ProcessConnect(thread: TConnectionThread); virtual; + procedure ProcessAccept(thread: TConnectionThread); virtual; + procedure Accept(aSocket: TLSocket); + procedure doDisconnect(aSocket: TLSocket); + procedure doConnect(aSocket: TLSocket); + procedure doTerminate;override; + procedure NetError(const msg: string; aSocket: TLSocket); + constructor Create(AThreadClass: TConnectionThreadClass; ALogger: TLogger; APort: integer); + destructor Destroy; override; + end; + + + { TClientRequest } + + +implementation +uses + lCommon; + + + +function TMainThread.getThread(index: TLSocket): TConnectionThread; +var + clt: TConnectionThread; + i: integer; +begin + for i := 0 to fclients.Count-1 do + if TConnectionThread(fclients[i]).Socket=index then + begin + result := TConnectionThread(fclients[i]); + log(self,format('getThread(%d) %s',[index.Handle,guidToString(result.ID)])); + exit; + end; + result := fThreadClass.Create(self,index); + log(self,format('new Thread(%d) %s',[index.Handle,guidToString(result.ID)])); + fclients.Add(Result); + +end; + +procedure TMainThread.TerminateClients; +var + i: integer; + clt: TConnectionThread; +begin + log(Self,'Terminate Clients'); + for i := fclients.Count-1 downto 0 do + begin + sleep(0); + clt := TConnectionThread(fclients[i]); + try + log(self,GuidToString(clt.ID)); + clt.Terminate; + clt.WaitFor; + clt.Free; + + except on e: Exception do + begin + log(self, '!!ERROR Destroy ' + e.Message); + end; + end; + end; + fClients.Clear; + +end; + +procedure TMainThread.Log(Sender: TObject; msg: string); +begin + if assigned(fLogger) then + fLogger(Sender,Msg); +end; + +procedure TMainThread.RemoveClient(clt: TConnectionThread); +begin + fclients.Remove(clt); +end; + + +procedure TMainThread.dataReady(aSocket: TLSocket); +var + clt: TConnectionThread; +begin + log(self,'dataReady'); + if Terminated then exit; + + clt := Client[aSocket]; + clt.Cache.WriteReady.WaitFor(INFINITE); + while clt.Cache.ReadFromSocket(aSocket)<>0 do + begin + sleep(0); + end; +end; + +procedure TMainThread.ProcessConnect(thread: TConnectionThread); +begin + +end; + +procedure TMainThread.ProcessAccept(thread: TConnectionThread); +begin + +end; + +procedure TMainThread.Accept(aSocket: TLSocket); +var + clt: TConnectionThread; +begin + log(self,'connect'); + if Terminated then exit; + clt := Client[aSocket]; + log(self,format('connected %s on %d ',[GUIDToString(clt.ID), aSocket.Handle])); + + ProcessAccept(clt); + clt.start; +end; + +procedure TMainThread.doDisconnect(aSocket: TLSocket); +var + clt: TConnectionThread; +begin + if terminated then exit; + log(self,'disconnect'); + try + clt := Client[aSocket]; + if clt.terminated then exit; + log(self,format('disconnected %s on %d ',[GUIDToString(clt.ID), aSocket.Handle])); + clt.Terminate; + clt.WaitFor; + clt.free; + fclients.remove(clt); + + except on e: Exception do + begin + log(self,'!!ERROR doDisconnect '+e.Message); + raise; + end; + end; +end; + +procedure TMainThread.doConnect(aSocket: TLSocket); +var + clt: TConnectionThread; +begin + log(self,'doConnect'); + if Terminated then exit; + clt := Client[aSocket]; + log(self,format('connected %s on %d ',[GUIDToString(clt.ID), aSocket.Handle])); + ProcessConnect(clt); + clt.Start; +end; + +procedure TMainThread.doTerminate; +begin + inherited doTerminate(); + TerminateClients; +end; + + + + +procedure TMainThread.NetError(const msg: string; aSocket: TLSocket); +begin + if assigned(aSocket) then + log(self,'!!NETERROR on '+inttostr(aSocket.Handle)+#09+msg) + else + log(self,'!!NETERROR '+msg); +end; + +constructor TMainThread.Create(AThreadClass: TConnectionThreadClass; + ALogger: TLogger; APort: integer); +begin + inherited Create(true); + fThreadClass:=AThreadClass; + fCon := TLTcp.Create(nil); + fclients := TList.Create; + fLogger := ALogger; + fPort := APort; + + Connect.OnDisconnect:=@doDisconnect; + Connect.OnReceive:=@dataReady; + Connect.Timeout:=100; + log(self,'create main thread'); +end; + +destructor TMainThread.Destroy; +begin + fClients.Free; + fCon.Free; + Inherited Destroy; +end; + + + +{ TConnectionThread } + + + + +class function TConnectionThread.BufferToString(const Buffer: TBuffer; + const len: integer): string; +var + i: integer; +begin + result := ''; + for i := 1 to len do + begin + result := result + IntToHex(Buffer[i-1],2)+' '; + if (i mod 64)=0 then + result := result +#13#10; + end; + +end; + +class procedure TConnectionThread.AddToBuffer(const Value: byte; + var Buffer: TBuffer; var pos: integer); +begin + Buffer[pos] := Value; + inc(pos); +end; + +class procedure TConnectionThread.AddToBuffer(const Value: word; + var Buffer: TBuffer; var pos: integer); +var + i: integer; + b: byte; +begin + for i := 0 to 1 do + begin + b := (Value shr (8*i)) and $FF; + Buffer[pos] := b; + inc(pos); + end; +end; + + +class procedure TConnectionThread.AddToBuffer(const Value: dword; + var Buffer: TBuffer; var pos: integer); +var + i: integer; + b: byte; +begin + for i := 0 to 3 do + begin + b := (Value shr (8*i)) and $FF; + Buffer[pos] := b; + inc(pos); + end; +end; + +class procedure TConnectionThread.AddToBuffer(const Value: QWord; + var Buffer: TBuffer; var pos: integer); +var + i: integer; + b: Byte; +begin + for i := 0 to 7 do + begin + b := (Value shr (8*i)) and $FF; + Buffer[pos] := b; + inc(pos); + end; +end; + +class procedure TConnectionThread.AddToBuffer(const Value: string; + var Buffer: TBuffer; var pos: integer); +var + len,i: DWORD; + p: PChar; + b: byte; +begin + len := Length(Value); + AddToBuffer(Len,Buffer,pos); + p := PChar(Value); + for i := 1 to len do + begin + b :=byte(p^); + AddToBuffer(b,Buffer,pos); + inc(p); + end; +end; + +class procedure TConnectionThread.AddToBuffer(const Value: TGUID; + var Buffer: TBuffer; var pos: integer); +var + i: integer; +begin + AddToBuffer(Value.D1,Buffer,pos); + AddToBuffer(Value.D2,Buffer,pos); + AddToBuffer(Value.D3,Buffer,pos); + for i := 0 to 7 do + AddToBuffer(Value.D4[i],Buffer,pos); +end; + +class procedure TConnectionThread.AddToBuffer(const Value: TBuffer; + var Buffer: TBuffer; var pos: integer); +var + len: DWORD; + i: integer; +begin + len := length(Value); + AddToBuffer(len,Buffer,pos); + for i := 0 to len-1 do + AddToBuffer(Value[i],Buffer,pos); +end; + +class procedure TConnectionThread.AddToBuffer(const Value: TStream; + var Buffer: TBuffer; var pos: integer); +var + len: QWORD; + i: integer; + b: byte; +begin + len := Value.Size; + AddToBuffer(len,Buffer,pos); + Value.seek(0,soFromBeginning); + for i := 0 to len-1 do + begin + Value.Read(b,1); + AddToBuffer(b,Buffer,pos); + end; +end; + +class procedure TConnectionThread.AddToBuffer(const Value: TParamArray; + var Buffer: TBuffer; var pos: integer); +var + l,i: DWORD; +begin + l := Length(Value); + AddToBuffer(l,Buffer,pos); + for i := low(Value) to high(Value) do + AddToBuffer(Value[i],Buffer,pos); +end; + +class procedure TConnectionThread.ReadFromBuffer(var Value: byte; + const Buffer: TBuffer; var pos: integer); +begin + Value := Buffer[pos]; + inc(pos); +end; + +class procedure TConnectionThread.ReadFromBuffer(var Value: word; + const Buffer: TBuffer; var pos: integer); +var + i: integer; +begin + Value := 0; + for i := 0 to 1 do + begin + Value := Value or (Buffer[pos] shl (8*i)); + inc(pos); + end; +end; + +class procedure TConnectionThread.ReadFromBuffer(var Value: dword; + const Buffer: TBuffer; var pos: integer); +var + i: integer; +begin + Value := 0; + for i := 0 to 3 do + begin + Value := Value or (Buffer[pos] shl (8*i)); + inc(pos); + end; +end; + +class procedure TConnectionThread.ReadFromBuffer(var Value: QWord; + const Buffer: TBuffer; var pos: integer); +var + i: integer; +begin + Value := 0; + for i := 0 to 7 do + begin + Value := Value or (Buffer[pos] shl (8*i)); + inc(pos); + end; +end; + + +class procedure TConnectionThread.ReadFromBuffer(var Value: string; + const Buffer: TBuffer; var pos: integer); +var + len: DWORD; + i: integer; + p: PChar; +begin + ReadFromBuffer(Len,Buffer,pos); + Value := StringOfChar(' ',len); + for i := 1 to len do + begin + Value[i] := chr(Buffer[pos]); + inc(pos); + end; +end; + +class procedure TConnectionThread.ReadFromBuffer(var Value: TGUID; + const Buffer: TBuffer; var pos: integer); +var + i: integer; +begin + ReadFromBuffer(Value.D1,Buffer,pos); + ReadFromBuffer(Value.D2,Buffer,pos); + ReadFromBuffer(Value.D3,Buffer,pos); + for i := 0 to 7 do + ReadFromBuffer(Value.D4[i],Buffer,pos); +end; + +class procedure TConnectionThread.ReadFromBuffer(var Value: TBuffer; + const Buffer: TBuffer; var pos: integer); +var + len: DWORD; + i: integer; +begin + ReadFromBuffer(len,Buffer,pos); + setLength(Value,len); + for i := 0 to len-1 do + ReadFromBuffer(Value[i],Buffer,pos); +end; + +class procedure TConnectionThread.ReadFromBuffer(out Value: TStream; + const Buffer: TBuffer; var pos: integer); +var + len: QWORD; + i: integer; + b: byte; +begin + Value := TMemoryStream.Create; + ReadFromBuffer(len,Buffer,pos); + if len=0 then exit; + for i := 0 to len-1 do + begin + ReadFromBuffer(b,Buffer,pos); + Value.Write(b,1); + end; + Value.seek(0,soFromBeginning); +end; + +class procedure TConnectionThread.ReadFromBuffer(out Value: TStrings; + const Buffer: TBuffer; var pos: integer); +var + i,w,d: dword; + s: string; +begin + Value := TStringList.Create; + ReadFromBuffer(w,Buffer,pos); + if w=0 then exit; + for i := 0 to w-1 do + begin + ReadFromBuffer(s,Buffer,pos); + ReadFromBuffer(d,Buffer,pos); + Value.AddObject(s, TObject(PtrInt(d))); + end; +end; + +class procedure TConnectionThread.ReadFromBuffer(out Value: TParamArray; + const Buffer: TBuffer; var pos: integer); +var + l,i: DWORD; +begin + ReadFromBuffer(l,Buffer,pos); + SetLength(Value,l); + if l=0 then exit; + for i := 0 to l-1 do + ReadFromBuffer(Value[i],Buffer,pos); +end; + +class procedure TConnectionThread.InitBuffer(buffSize: dword; out + Buffer: TBuffer; out pos: integer); +begin + SetLength(Buffer,buffSize); + pos := 0; +end; + + +procedure TConnectionThread.log(msg: string); +begin + if assigned(fOwner) then + fOwner.log(self,Role+#09+ GuidToString(ID)+#09+msg); +end; + + + +procedure TConnectionThread.SendBuffer(const Buffer: TBuffer; Len: dword); +var + p,t: PByte; + l,rem,i: integer; + part_id,tmp: QWORD; + b2: array[0..7] of byte; +begin + log('Send buffer '+inttostr(len)); + try + rem := len+Sizeof(integer)+Sizeof(QWord); + p := GetMem(rem); + try + t := p; + CopyBytes(t,PacketStart); + CopyBytes(t,len); + CopyBytes(t,Buffer); + t := p; + repeat + l := Socket.send(t^,rem); + dec(rem,l); + inc(t,l); + if l=0 then + sleep(100); + until terminated or (rem<=0); + + finally + //log(format('%p',[p])); + freeMem(p); + end; + except on e:Exception do + begin + log('!!ERROR SendBuffer '+e.message); + raise; + end; + end; + +end; + +function TConnectionThread.ReceiveBuffer(var Buffer: TBuffer; out Len: dword + ): boolean; +var + p: PByte; + i,l,rem: integer; + lbytes: array[0..3] of byte; + b2: array[0..7] of byte; + part_id: QWORD; +begin + result := false; + if Terminated then exit; + try + Cache.Read(part_id); + if Part_id<>PacketStart then exit; + Cache.Read(len); + if len=0 then exit; + setlength(Buffer,len); + rem := len; + p := PByte(Buffer); + repeat + l := Cache.pop(p^,rem); + dec(rem,l); + inc(p,l); + if Terminated then exit; + until terminated or (rem<=0) ; + log('Receive buffer '+inttostr(len)); + result := true; + except on e:Exception do + begin + log('!!ERROR ReceiveBuffer '+e.message); + raise; + end; + end; +end; + + +procedure TConnectionThread.SendHeader(packetType, state: byte; Code: DWORD; + QP: QWORD; SP: string); +var + Buffer: TBuffer; + pos: integer; +begin + try + log(format('SendHeader(%d) Code=%d, Param=%x, Command=%s',[packettype,Code,QP,SP])); + InitBuffer(sizeof(BYTE)*(Length(SP)+2)+16+2*sizeof(QWord)+sizeof(DWORD)*2,Buffer,pos); + AddToBuffer(packetType,Buffer,pos); + AddToBuffer(state,Buffer,pos); + AddToBuffer(ID,Buffer,pos); + AddToBuffer(recNo,Buffer,pos); + AddToBuffer(Code,Buffer,pos); + AddToBuffer(QP,Buffer,pos); + AddToBuffer(SP,Buffer,pos); + SendBuffer(Buffer,pos); + + except on e:Exception do + begin + log('!!ERROR SendHeader '+e.message); + raise; + end; + end; +end; + +function TConnectionThread.ReceiveHeader(out packetType, state: byte; out + Sender: TGUID; out num: QWORD; out Code: DWORD; out QP: QWORD; out SP: string + ): boolean; +var + Buffer: TBuffer; + len: dword; + pos: integer; +begin + result := false; + if Terminated then exit; + try + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(packetType,Buffer,pos); + ReadFromBuffer(State,Buffer,pos); + ReadFromBuffer(Sender,Buffer,pos); + ReadFromBuffer(num,Buffer,pos); + ReadFromBuffer(Code,Buffer,pos); + ReadFromBuffer(QP,Buffer,pos); + ReadFromBuffer(SP,Buffer,pos); + log(format('ReceiveHeader(%d) Code=%d, Param=%x, Command=%s',[packettype,Code,QP,SP])); + result := true; + + except on e:Exception do + begin + log('!!ERROR ReceiveHeader '+e.message); + raise; + end; + end; +end; + + + +procedure TConnectionThread.SendMessage(const mode: byte; + const Status: DWORD; const QParam: QWord; const sParam: string); +begin + SendHeader(0,mode,Status,QParam,sParam); +end; + +procedure TConnectionThread.SendMessage(const mode: byte; + const Status: DWORD; const QParam: QWord; const AValue: string; + const AKeys: TStrings); +begin + if assigned(AKeys) then + begin + SendHeader(1,mode,Status,QParam,AValue); + SendData(1,AKeys); + end + else + SendHeader(0,mode,Status,QParam,AValue); +end; + + +procedure TConnectionThread.SendMessage(const mode: byte; + const CommandID: DWORD; const QParam: QWord; const AValue: string; + const AKeys: TStrings; const IntData: TParamArray; const AData: TStream); +begin + if assigned(AKeys) and assigned(AData) and (length(IntData)>0) then + begin + SendHeader(7,mode,CommandID,QParam,AValue); + SendData(1,AKeys); + SendData(2,IntData); + SendData(3,AData); + end + else if (length(IntData)>0) and assigned(AKeys) then + begin + SendHeader(6,mode,CommandID,QParam,AValue); + SendData(1,AKeys); + SendData(2,IntData); + end + else if (length(IntData)>0) and assigned(AData) then + begin + SendHeader(5,mode,CommandID,QParam,AValue); + SendData(1,IntData); + SendData(2,AData); + end + else if assigned(AKeys) and assigned(AData) then + begin + SendHeader(4,mode,CommandID,QParam,AValue); + SendData(1,AKeys); + SendData(2,AData); + end + else if length(IntData)>0 then + begin + SendHeader(3,mode,CommandID,QParam,AValue); + SendData(1,IntData); + end + else if assigned(AData) then + begin + SendHeader(2,mode,CommandID,QParam,AValue); + SendData(2,AData); + end + else if assigned(AKeys) then + begin + SendHeader(1,mode,CommandID,QParam,AValue); + SendData(1,AKeys); + end + else + SendHeader(0,mode,CommandID,QParam,AValue); +end; + + + + + + +procedure TConnectionThread.SendData(part: byte; const Data: TStream); +var + Buffer: TBuffer; + pos: integer; + footer: dWORD; +begin + try + setLength(Buffer,1+Data.Size+8); + pos := 0; + AddToBuffer(part,Buffer,pos); + AddToBuffer(Data,Buffer,pos); + SendBuffer(Buffer,pos); + + except on e:Exception do + begin + raise; + end; + end; +end; + +procedure TConnectionThread.SendData(part: byte; const Data: TBuffer); +var + Buffer: TBuffer; + pos: integer; + footer: dWORD; +begin + try + setLength(Buffer,length(Data)+4+1); + pos := 0; + AddToBuffer(part,Buffer,pos); + AddToBuffer(Data,Buffer,pos); + SendBuffer(Buffer,pos); + + except on e:Exception do + begin + raise; + end; + end; +end; + +procedure TConnectionThread.SendData(part: byte; const Data: TStrings); +var + Buffer: TBuffer; + pos: integer; + w,footer: dWORD; + len,i: integer; +begin + try + LogStrings(fOwner.flogger,self,'KEYS',Data); + len := 1+4+8*Data.Count; + for i:=0 to Data.Count-1 do + inc(len,length(Data[i])); + setLength(Buffer,len); + pos := 0; + AddToBuffer(part,Buffer,pos); + w := Data.Count; + AddToBuffer(w,Buffer,pos); + for i := 0 to Data.Count-1 do + begin + AddToBuffer(Data[i],Buffer,pos); + w := ptrInt(Pointer(Data.Objects[i])) and $FFFFFFFF; + AddToBuffer(w,Buffer,pos); + end; + SendBuffer(Buffer,pos); + + except on e:Exception do + begin + raise; + end; + end; +end; + +procedure TConnectionThread.SendData(part: byte; + const Data: TParamArray); +var + i,pos: integer; + len: DWORD; + Buffer: TBuffer; +begin + len := length(Data); + InitBuffer(Sizeof(byte)+(len+1)*SizeOf(DWORD),Buffer,pos); + AddToBuffer(part,Buffer,pos); + AddToBuffer(len,Buffer,pos); + for i := low(Data) to High(Data) do + AddToBuffer(Data[i],Buffer,pos); + SendBuffer(Buffer,pos); +end; + + +function TConnectionThread.ReceiveMessage(out mode: byte; out Sender: TGUID; + out rNum: QWord; out CommandID: DWORD; out QParam: QWord; out Value: string; + out intData: TParamArray; out Keys: TStrings; out Data: TStream): boolean; +var + Buffer: TBuffer; + Len: dword; + pos: integer; + s: string; + b,b1: byte; +begin + result := false; + if Terminated then exit; + try + log('ReceiveMessage'); + if not ReceiveHeader(b,mode,Sender,rNum,CommandID,QParam,Value) then exit; + if Terminated then exit; + case b of + 1: begin + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(b,Buffer,pos); + if b<>1 then raise EFormatException.Create(''); + ReadFromBuffer(Keys,Buffer,pos); + LogStrings(fOwner.flogger,self,'KEYS',Keys); + end; + 2: begin + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(b,Buffer,pos); + if b<>1 then raise EFormatException.Create(''); + ReadFromBuffer(Data,Buffer,pos); + end; + 3: begin + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(b,Buffer,pos); + if b<>1 then raise EFormatException.Create(''); + ReadFromBuffer(intData,Buffer,pos); + end; + 4: begin + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(b,Buffer,pos); + if b<>1 then raise EFormatException.Create(''); + ReadFromBuffer(Keys,Buffer,pos); + ReadFromBuffer(b,Buffer,pos); + if b<>2 then raise EFormatException.Create(''); + ReadFromBuffer(Data,Buffer,pos); + end; + 5: begin + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(b,Buffer,pos); + if b<>1 then raise EFormatException.Create(''); + ReadFromBuffer(intData,Buffer,pos); + ReadFromBuffer(b,Buffer,pos); + if b<>2 then raise EFormatException.Create(''); + ReadFromBuffer(Data,Buffer,pos); + end; + 6: begin + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(b,Buffer,pos); + if b<>1 then raise EFormatException.Create(''); + ReadFromBuffer(Keys,Buffer,pos); + ReadFromBuffer(b,Buffer,pos); + if b<>2 then raise EFormatException.Create(''); + ReadFromBuffer(intData,Buffer,pos); + end; + 7: begin + if not ReceiveBuffer(Buffer,len) then exit; + pos := 0; + ReadFromBuffer(b,Buffer,pos); + if b<>1 then raise EFormatException.Create(''); + ReadFromBuffer(Keys,Buffer,pos); + ReadFromBuffer(b,Buffer,pos); + if b<>2 then raise EFormatException.Create(''); + ReadFromBuffer(intData,Buffer,pos); + ReadFromBuffer(b,Buffer,pos); + if b<>3 then raise EFormatException.Create(''); + ReadFromBuffer(Data,Buffer,pos); + end; + end; + result := true; + + except on e:Exception do + begin + log('!!ERROR ReceiveMessage '+e.message); + raise; + end; + end; +end; + +constructor TConnectionThread.Create(Aowner: TMainThread; ASocket: TLSocket); +var + i,d1,d2 : integer; +begin + inherited Create(true); + fCache := TRoundBuffer.Create(10000); + fSocket := ASocket; + fOwner := AOwner; + CreateGuid(ID); + recNo := 0; + log('Create'); +end; + +destructor TConnectionThread.Destroy; +begin + log('Destroy'); + fCache.Free; + fOwner.removeClient(self); + log('Destroy2'); + inherited Destroy; +end; + +procedure TConnectionThread.Execute; +var + Sender: TGUID; + num,Param: QWORD; + CommandID: DWORD; + Value: string; + intData: TParamArray; + Keys: TStrings; + Data: TStream; + mode: byte; +begin + log('start thread'); + while not terminated do + begin + if cache.ReadReady.WaitFor(1000)<>wrSignaled then begin sleep(10);continue;end; + log('received'); + if terminated then break; + if not Socket.Connected then break; + Keys := nil; + Data := nil; + try + if ReceiveMessage(mode,Sender,num,CommandID,Param,Value,intData,Keys,Data) then + ProcessMessage(mode,CommandID,Param,Value,Keys,intData,Data); + + finally + if assigned(Keys) then Keys.Free; + if assigned(Data) then Data.Free; + setLength(intData,0); + end; + end; + Cache.Close; + Socket.Disconnect(); + log('terminated'); +end; + +procedure TConnectionThread.TerminatedSet; +begin + log('terminate required'); + Cache.Close; +end; + +{ TClientThread } + + + +const + HexChars='0123456789abcdef'; + + + + + + + + + +end. + diff --git a/xpReportUtil.pas b/xpReportUtil.pas new file mode 100644 index 0000000..2e97c0e --- /dev/null +++ b/xpReportUtil.pas @@ -0,0 +1,127 @@ +unit xpReportUtil; + +interface + +uses Classes, AbUnzper,AbZipper; + +procedure PackReport(SrcStream, DestStream : TStream; Zipper: TAbZipper); +procedure UnpackReport(SrcStream, DestStream : TStream; UnZipper: TAbUnZipper); + +implementation + +uses zipper,sysutils, ABUtils, LazUTF8; +type + + { TZipTool } + + TZipTool=class + private + fSrcStream: TStream; + fSrcStrean, + fDstStream: TStream; + public + constructor Create(ASourceStream,ADestStream: TStream); + Procedure getSource(Sender: TObject; var AStream: TStream); + Procedure getDest(Sender : TObject; var AStream : TStream; AItem : TFullZipFileEntry); + Procedure closeSource(Sender: TObject; var AStream: TStream); + Procedure doneDest(Sender : TObject; var AStream : TStream; AItem : TFullZipFileEntry); + Procedure endFile(Sender : TObject; Const Ratio : Double); + Procedure progress(Sender : TObject; Const Pct : Double); + property SourceStream: TStream read fSrcStream; + property DestStream:TStream read fDstStream; + end; + +procedure UnpackReport(SrcStream, DestStream : TStream; UnZipper: TAbUnZipper); +var + rptCode: TStringList; + i: integer; + tmp: TStream; +begin + if SrcStream.Size > 0 then + begin + tmp := TMemoryStream.Create; + rptCode := TStringList.Create; + try + UnZipper.Stream := SrcStream; + UnZipper.ArchiveType := atZip; + UnZipper.ForceType := true; + UnZipper.ExtractToStream('Q.Q',tmp); + + tmp.Seek(0,soFromBeginning); + rptCode.LoadFromStream(tmp); + // автозамена шрифтов + for i := 0 to rptCode.Count-1 do + begin + {$IFDEF LINUX} + rptCode[i] := UTF8StringReplace(rptCode[i],'Font.Name="Times New Roman"','Font.Name="PT Astra Serif"',[]); + rptCode[i] := UTF8StringReplace(rptCode[i],'Font.Name="Arial"','Font.Name="Liberation Sans"',[]); + rptCode[i] := UTF8StringReplace(rptCode[i],'Rotation="90"','Rotation="90" Wysiwyg="0"',[]); + rptCode[i] := UTF8StringReplace(rptCode[i],'Wysiwyg="0" Wysiwyg="0"','Wysiwyg="0"',[]); + rptCode[i] := UTF8StringReplace(rptCode[i],'Rotation="90" Wysiwyg="0" Wysiwyg="1"','Rotation="90" Wysiwyg="0"',[]); + {$ELSE} + rptCode[i] := UTF8StringReplace(rptCode[i],'Font.Name="PT Astra Serif"','Font.Name="Times New Roman"',[]); + rptCode[i] := UTF8StringReplace(rptCode[i],'Font.Name="Liberation Sans"','Font.Name="Arial"',[]); + {$ENDIF} + end; + rptCode.SaveToStream(DestStream); + + finally + rptCode.Free; + tmp.Free; + end; + + end; // if + +end; + +procedure PackReport(SrcStream, DestStream : TStream; Zipper: TAbZipper); +begin + zipper.FileName := ''; + zipper.ArchiveType := atZip; + zipper.ForceType := true; + + + zipper.Stream := DestStream; + zipper.AddFromStream('Q.Q',SrcStream); + + zipper.Save; +end; + +{ TZipTool } + +constructor TZipTool.Create(ASourceStream, ADestStream: TStream); +begin + inherited Create; + fSrcStream := ASourceStream; + fDstStream := ADestStream; +end; + +procedure TZipTool.getSource(Sender: TObject; var AStream: TStream); +begin + AStream := fSrcStream; +end; + +procedure TZipTool.getDest(Sender: TObject; var AStream: TStream; + AItem: TFullZipFileEntry); +begin + AStream := fDstStream; +end; + +procedure TZipTool.closeSource(Sender: TObject; var AStream: TStream); +begin +end; + +procedure TZipTool.doneDest(Sender: TObject; var AStream: TStream; + AItem: TFullZipFileEntry); +begin +end; + +procedure TZipTool.endFile(Sender: TObject; const Ratio: Double); +begin +end; + +procedure TZipTool.progress(Sender: TObject; const Pct: Double); +begin +end; + +end. diff --git a/xpaccessunit.pas b/xpaccessunit.pas new file mode 100644 index 0000000..afa8722 --- /dev/null +++ b/xpaccessunit.pas @@ -0,0 +1,43 @@ +unit xpAccessUnit; +{$H+} +interface + + function EncryptText(const AText: String) : String; + function MySQLPassword(const AText: Ansistring) : Ansistring; +implementation + +uses + SysUtils, + Variants, DCPsha1, DCPdes, + ConnectionsDmUnit; + + +function DecryptText(const AText: string) : string; +begin + Result := AText; +end; +function EncryptText(const AText: String) : String; +begin + Result := String(MySQLPassword(AnsiString(StringReplace(AText, '\', '\\', [rfReplaceAll, rfIgnoreCase])))); +end; + + +function MySQLPassword(const AText: Ansistring) : Ansistring; +var + Digest: packed array[0..19] of byte; + i: integer; +begin + Result := '*'; + ConnectionsDM.Hash.Init; + ConnectionsDM.Hash.UpdateStr(AText); + ConnectionsDM.Hash.Final(Digest); + ConnectionsDM.Hash.Burn; + ConnectionsDM.Hash.Init; + ConnectionsDM.Hash.Update(Digest,20); + ConnectionsDM.Hash.Final(Digest); + ConnectionsDM.Hash.Burn; + for i:= 0 to 19 do + Result := Result + AnsiString(IntToHex(Digest[i], 2)); +end; + +end. diff --git a/xpmemparammanagerunit.pas b/xpmemparammanagerunit.pas new file mode 100644 index 0000000..2805b25 --- /dev/null +++ b/xpmemparammanagerunit.pas @@ -0,0 +1,141 @@ +unit xpMemParamManagerUnit; + +interface + +uses + Messages, SysUtils, Variants, Classes; +// xpConst; +const + sFRBreak = #13#10; +type + + ParamRow = array[0..1] of Variant; + TParamArray = array of ParamRow; + + { TxpMemParamManager } + + TxpMemParamManager = class(TObject) + private + ParamArray: TParamArray; + function GetValue(ParamName: Variant): Variant; + procedure SetValue(ParamName: Variant; const Value: Variant); + + public + constructor Create; + destructor Destroy; override; + function IndexOf(const ParamName : string) : Integer; + function Count: Integer; + function ListAllParamsAndValues: string; + procedure Delete(ParamName: Variant); + property Params: TParamArray read ParamArray; + property Values[ParamName: Variant]: Variant read GetValue write SetValue; default; + procedure Assign(OwnerParam: TxpMemParamManager); + + end; + +implementation +uses + lazUTF8; +procedure TxpMemParamManager.Assign(OwnerParam: TxpMemParamManager); +var + i: Integer; +begin + if OwnerParam = nil then + Exit; + if Length(OwnerParam.ParamArray) > 0 then + for i:= Low(OwnerParam.ParamArray) to High(OwnerParam.ParamArray) do + Values[OwnerParam.ParamArray[i][0]] := OwnerParam.ParamArray[i][1] +end; + +constructor TxpMemParamManager.Create; +begin + inherited Create; + ParamArray := nil; +end; + +destructor TxpMemParamManager.Destroy; +begin + ParamArray := nil; + inherited; +end; + +function TxpMemParamManager.GetValue(ParamName: Variant): Variant; +var + i: Integer; +begin + for i:= Low(ParamArray) to High(ParamArray) do + if AnsiSameText(ParamArray[i][0], ParamName) then + begin + Result := ParamArray[i][1]; + Exit; + end; + raise Exception.Create('Не найден параметр ' + VarToStr(ParamName)); +end; + +function TxpMemParamManager.IndexOf(const ParamName: string): Integer; +var + i : Integer; +begin + Result := -1; + for i := Low(ParamArray) to High(ParamArray) do + begin + if AnsiSameText(ParamArray[i][0], ParamName) then + begin + Result := i; + Exit; + end; + end; +end; + +function TxpMemParamManager.Count: Integer; +begin + result := High(ParamArray) + 1; +end; + +function TxpMemParamManager.ListAllParamsAndValues: string; +var + i: integer; +begin + Result := ''; + for i := 0 to (self.Count - 1) do + Result := Result + IntToStr(i) + ': '#39 + VarToStr(self.Params[i][0]) + #39' = '#39 + VarToStr(self.Params[i][1]) + #39'.' + sLineBreak; +end; + +procedure TxpMemParamManager.SetValue(ParamName: Variant; const Value: Variant); +var + i,j: Integer; + strVal: string; + v: variant; +begin + if VarIsStr(Value) then + begin + v := utf8trim(Value); + end + else + v := Value; + for i:= Low(ParamArray) to High(ParamArray) do + if AnsiSameText(ParamArray[i][0], ParamName) then + begin + ParamArray[i][1] := v; + Exit; + end; + SetLength(ParamArray, Length(ParamArray) + 1); + ParamArray[High(ParamArray)][0] := ParamName; + ParamArray[High(ParamArray)][1] := v; +end; + + +procedure TxpMemParamManager.Delete(ParamName: Variant); +var + i: Integer; +begin + for i:= Low(ParamArray) to High(ParamArray) do + if AnsiSameText(ParamArray[i][0], ParamName) then + begin + ParamArray[i][0] := ''; + Exit; + end; +end; + +end. + diff --git a/xputilunit.pas b/xputilunit.pas new file mode 100644 index 0000000..c605e32 --- /dev/null +++ b/xputilunit.pas @@ -0,0 +1,69 @@ +unit xpUtilUnit; + +{$mode ObjFPC}{$H+} + +interface + +uses + Classes, SysUtils; +function inArrayPos(const value: string; const checkList: array of string; ignoreCase: boolean): integer; +// проверяет содержится ли строка в массиве +function inArray(const value: string; const checkList: array of string; ignoreCase: boolean): boolean; +// находит индекс числа в массиве +function inArrayPosN(const value: integer; const checkList: array of integer): integer; +// проверяет содержится ли число в массиве +function inArrayN(const value: integer; const checkList: array of integer): boolean; + +implementation +uses + lazUTF8; +function inArrayPos(const value: string; const checkList: array of string; + ignoreCase: boolean): integer; +var + i: integer; + s1,s2: string; +begin + result := -1; + + if ignoreCase then + s1 := UTF8UpperString(value) + else + s1 := value; + for I := Low(checkList) to High(checkList) do + begin + if ignoreCase then + s2 := UTF8UpperString(checklist[i]) + else + s2 := checklist[i]; + if SameStr(s1, s2) then + exit(I); + end; +end; + + +function inArray(const value: string; const checkList: array of string; + ignoreCase: boolean): boolean; +begin + result := inArrayPos(value, checkList, ignoreCase)>=0; +end; + +function inArrayPosN(const value: integer; const checkList: array of integer + ): integer; +var + i: integer; +begin + result := -1; + for I := Low(checkList) to High(checkList) do + if value = checkList[i] then + exit(I); +end; + + +function inArrayN(const value: integer; const checkList: array of integer + ): boolean; +begin + result := inArrayPosN(value, checkList)>=0; +end; + +end. +