Subversion Repositories currency_converter

Rev

Rev 22 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 daniel-mar 1
unit VtsCurConv;
2
 
3
interface
4
 
5
uses
18 daniel-mar 6
  SysUtils, Classes;
2 daniel-mar 7
 
8
type
9
  EVtsCurConvException = class(Exception);
10
 
11
  TVtsCurApiKey = string;
12
  TVtsCur = string;
13
  TVtsRate = double;
14
 
15
  TVtsCurConv = class(TObject)
16
  private
17
    FSecure: boolean;
18
    FMaxAgeSeconds: integer;
19
    FConfirmWebAccess: boolean;
20
    FFallBackToCache: boolean;
21
    FInteractiveAPIKeyInput: boolean;
22
  protected
23
    function GetJsonRaw(HistoricDate: TDate=0): string;
24
    procedure QueryAPIKey(msg: string=''); virtual;
25
  public
26
    property Secure: boolean read FSecure write FSecure;
27
    property MaxAgeSeconds: integer read FMaxAgeSeconds write FMaxAgeSeconds;
28
    property ConfirmWebAccess: boolean read FConfirmWebAccess write FConfirmWebAccess;
29
    property FallBackToCache: boolean read FFallBackToCache write FFallBackToCache;
30
    property InteractiveAPIKeyInput: boolean read FInteractiveAPIKeyInput write FInteractiveAPIKeyInput;
31
    class procedure WriteAPIKey(key: TVtsCurApiKey; UserMode: boolean=true);
32
    class function ReadAPIKey: TVtsCurApiKey;
33
    class function DeleteAPIKey(UserMode: boolean=true): boolean;
34
    function Convert(value: Currency; fromCur, toCur: TVtsCur; HistoricDate: TDate=0): Currency;
35
    function GetAcceptedCurrencies(sl: TStringList=nil; HistoricDate: TDate=0): integer;
36
  end;
37
 
38
implementation
39
 
40
uses
41
  Windows, Registry, uLkJSON, Dialogs, IdHTTP, DateUtils;
42
 
43
function FileGetContents(filename: string): string;
44
var
45
  sl: TStringList;
46
begin
47
  sl := TStringList.Create;
48
  try
49
    sl.LoadFromFile(filename);
50
    result := sl.Text;
51
  finally
52
    sl.Free;
53
  end;
54
end;
55
 
56
procedure FilePutContents(filename, content: string);
57
var
58
  sl: TStringList;
59
begin
60
  sl := TStringList.Create;
61
  try
62
    sl.Text := content;
63
    sl.SaveToFile(filename);
64
  finally
65
    sl.Free;
66
  end;
67
end;
68
 
69
function GetPage(aURL: string): string;
70
var
71
  Response: TStringStream;
72
  HTTP: TIdHTTP;
73
const
74
  HTTP_RESPONSE_OK = 200;
13 daniel-mar 75
resourcestring
76
  S_CANNOT_DOWNLOAD = 'Cannot download from %s';
2 daniel-mar 77
begin
78
  // https://stackoverflow.com/questions/9239267/how-to-download-a-web-page-into-a-variable
79
  Result := '';
80
  Response := TStringStream.Create('');
81
  try
82
    HTTP := TIdHTTP.Create(nil);
83
    try
84
      HTTP.Get(aURL, Response);
85
      if HTTP.ResponseCode = HTTP_RESPONSE_OK then
86
        Result := Response.DataString
87
      else
13 daniel-mar 88
        raise EVtsCurConvException.CreateFmt(S_CANNOT_DOWNLOAD, [aURL]);
2 daniel-mar 89
    finally
90
      HTTP.Free;
91
    end;
92
  finally
93
    Response.Free;
94
  end;
95
end;
96
 
97
function GetTempDir: string;
98
var
99
  Dir: string;
100
  Len: DWord;
101
begin
102
  SetLength(Dir, MAX_PATH);
103
  Len := GetTempPath(MAX_PATH, PChar(Dir));
104
  if Len > 0 then
105
  begin
106
    SetLength(Dir, Len);
107
    Result := Dir;
108
  end
109
  else
110
    RaiseLastOSError;
111
end;
112
 
113
{ TVtsCurConv }
114
 
115
class procedure TVtsCurConv.WriteAPIKey(key: TVtsCurApiKey; UserMode: boolean=true);
116
  procedure _WriteAPIKey(root: HKEY);
117
  var
118
    reg: TRegistry;
13 daniel-mar 119
  resourcestring
120
    S_CANNOT_OPEN_REGISTRY = 'Cannot open registry key';
2 daniel-mar 121
  begin
122
    reg := TRegistry.Create;
123
    try
124
      reg.RootKey := root;
125
      if reg.OpenKey('Software\ViaThinkSoft\CurrencyConverter', true) then
126
      begin
127
        reg.WriteString('APIKey', key);
128
        reg.CloseKey;
129
      end
13 daniel-mar 130
      else raise EVtsCurConvException.Create(S_CANNOT_OPEN_REGISTRY);
2 daniel-mar 131
    finally
132
      reg.Free;
133
    end;
134
  end;
135
begin
136
  if UserMode then
137
    _WriteAPIKey(HKEY_CURRENT_USER)
138
  else
139
    _WriteAPIKey(HKEY_LOCAL_MACHINE);
140
end;
141
 
142
class function TVtsCurConv.DeleteAPIKey(UserMode: boolean=true): boolean;
143
  procedure _DeleteAPIKey(root: HKEY);
144
  var
145
    reg: TRegistry;
146
  begin
147
    reg := TRegistry.Create;
148
    try
149
      reg.RootKey := root;
150
      if reg.OpenKey('Software\ViaThinkSoft\CurrencyConverter', true) then
151
      begin
152
        result := reg.DeleteValue('APIKey');
153
        reg.CloseKey;
154
      end;
155
    finally
156
      reg.Free;
157
    end;
158
  end;
159
begin
160
  result := false;
161
  if UserMode then
162
    _DeleteAPIKey(HKEY_CURRENT_USER)
163
  else
164
    _DeleteAPIKey(HKEY_LOCAL_MACHINE);
165
end;
166
 
167
class function TVtsCurConv.ReadAPIKey: TVtsCurApiKey;
168
  function _ReadAPIKey(root: HKEY): string;
169
  var
170
    reg: TRegistry;
171
  begin
172
    result := '';
173
    reg := TRegistry.Create;
174
    try
175
      reg.RootKey := root;
176
      if reg.OpenKeyReadOnly('Software\ViaThinkSoft\CurrencyConverter') then
177
      begin
178
        if reg.ValueExists('APIKey') then result := reg.ReadString('APIKey');
179
        reg.CloseKey;
180
      end;
181
    finally
182
      reg.Free;
183
    end;
184
  end;
185
begin
186
  result := _ReadAPIKey(HKEY_CURRENT_USER);
187
  if result = '' then result := _ReadAPIKey(HKEY_LOCAL_MACHINE);
188
end;
189
 
190
function TVtsCurConv.Convert(value: Currency; fromCur, toCur: TVtsCur; HistoricDate: TDate=0): Currency;
191
var
192
  rateTo, rateFrom: TVtsRate;
193
  i: Integer;
194
  rateToFound: Boolean;
195
  rateFromFound: Boolean;
196
  sJSON: String;
197
  xSource: TlkJSONstring;
198
  xRoot: TlkJSONobject;
199
  xQuotes: TlkJSONobject;
200
  xRate: TlkJSONnumber;
13 daniel-mar 201
resourcestring
202
  S_JSON_ENTRY_MISSING = 'JSON entry "%s" is missing!';
203
  S_WRONG_QUOTE_LEN = 'Length of quotes-entry is unexpected!';
204
  S_JSON_RATE_MISSING = 'JSON entry quotes->rate is missing!';
205
  S_CURRENCY_NOT_SUPPORTED = 'Currency "%s" not supported';
2 daniel-mar 206
begin
207
  result := 0; // to avoid that the compiler shows a warning
208
 
209
  fromCur := Trim(UpperCase(fromCur));
210
  toCur := Trim(UpperCase(toCur));
16 daniel-mar 211
 
212
  if fromCur = toCur then
213
  begin
214
    result := value;
215
    exit;
216
  end;
2 daniel-mar 217
 
218
  sJSON := GetJsonRaw(HistoricDate);
219
 
220
  xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
221
  try
222
    xSource := xRoot.Field['source'] as TlkJSONstring;
13 daniel-mar 223
    if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
2 daniel-mar 224
 
225
    xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
13 daniel-mar 226
    if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
2 daniel-mar 227
 
228
    rateToFound := false;
229
    rateFromFound := false;
230
    rateTo := 0.00; // to avoid that the compiler shows a warning
231
    rateFrom := 0.00; // to avoid that the compiler shows a warning
232
 
233
    for i := 0 to xQuotes.Count - 1 do
234
    begin
13 daniel-mar 235
      if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
2 daniel-mar 236
 
237
      xRate := xQuotes.Field[xQuotes.NameOf[i]] as TlkJSONnumber;
13 daniel-mar 238
      if not Assigned(xRate) then raise EVtsCurConvException.Create(S_JSON_RATE_MISSING);
2 daniel-mar 239
 
240
      if Copy(xQuotes.NameOf[i], 1, 3) = xSource.Value then
241
      begin
242
        if Copy(xQuotes.NameOf[i], 4, 3) = toCur then
243
        begin
244
          rateTo := xRate.Value;
245
          rateToFound := true;
246
        end;
247
        if Copy(xQuotes.NameOf[i], 4, 3) = fromCur then
248
        begin
249
          rateFrom := xRate.Value;
250
          rateFromFound := true;
251
        end;
252
      end;
253
    end;
254
 
13 daniel-mar 255
    if not rateToFound then raise EVtsCurConvException.CreateFmt(S_CURRENCY_NOT_SUPPORTED, [toCur]);
256
    if not rateFromFound then raise EVtsCurConvException.CreateFmt(S_CURRENCY_NOT_SUPPORTED, [fromCur]);
2 daniel-mar 257
 
258
    result := value * rateTo / rateFrom;
259
  finally
260
    xRoot.Free;
261
  end;
262
end;
263
 
264
procedure TVtsCurConv.QueryAPIKey(msg: string='');
265
var
266
  s: string;
13 daniel-mar 267
resourcestring
268
  S_CURRENCYLAYER = 'currencylayer.com';
269
  S_ENTER_KEY = 'Please enter your API key:';
270
  S_NO_API_KEY = 'No API key provided.';
2 daniel-mar 271
begin
13 daniel-mar 272
  s := Trim(InputBox(S_CURRENCYLAYER, Trim(msg + ' ' + S_ENTER_KEY), ''));
273
  if s = '' then raise EVtsCurConvException.Create(S_NO_API_KEY);
2 daniel-mar 274
  WriteAPIKey(s);
275
end;
276
 
277
function TVtsCurConv.GetAcceptedCurrencies(sl: TStringList=nil; HistoricDate: TDate=0): integer;
278
var
279
  i: Integer;
280
  sJSON: String;
281
  xSource: TlkJSONstring;
282
  xRoot: TlkJSONobject;
283
  xQuotes: TlkJSONobject;
13 daniel-mar 284
resourcestring
285
  S_JSON_ENTRY_MISSING = 'JSON entry "%s" is missing!';
286
  S_WRONG_QUOTE_LEN = 'Length of quotes-entry is unexpected!';
2 daniel-mar 287
begin
288
  result := 0;
289
 
290
  sJSON := GetJsonRaw(HistoricDate);
291
 
292
  xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
293
  try
294
    xSource := xRoot.Field['source'] as TlkJSONstring;
13 daniel-mar 295
    if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
2 daniel-mar 296
 
297
    xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
13 daniel-mar 298
    if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
2 daniel-mar 299
 
300
    for i := 0 to xQuotes.Count - 1 do
301
    begin
13 daniel-mar 302
      if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
2 daniel-mar 303
 
304
      if Copy(xQuotes.NameOf[i], 1, 3) = xSource.Value then
305
      begin
306
        Inc(result);
307
        if Assigned(sl) then sl.Add(Copy(xQuotes.NameOf[i], 4, 3));
308
      end;
309
    end;
310
  finally
311
    xRoot.Free;
312
  end;
313
end;
314
 
315
function TVtsCurConv.GetJsonRaw(HistoricDate: TDate=0): string;
316
 
317
  procedure _HandleKeyInvalidOrMissing(cacheFileName: string; msg: string; out doRetry: boolean; out json: string);
13 daniel-mar 318
  resourcestring
319
    S_ENTER_NEW_KEY = 'Do you want to enter a new one?';
17 daniel-mar 320
    S_NO_CACHE_EXISTING = 'Cannot get the data online, and no cache exists. Please check your internet connection and API key.';
2 daniel-mar 321
  begin
322
    if FallBackToCache then
323
    begin
324
      if not InteractiveAPIKeyInput then
325
      begin
17 daniel-mar 326
        if not FileExists(cacheFileName) then
327
        begin
328
          raise Exception.Create(S_NO_CACHE_EXISTING);
329
        end;
2 daniel-mar 330
        json := FileGetContents(cacheFileName);
331
        doRetry := false;
332
      end
333
      else
334
      begin
13 daniel-mar 335
        if MessageDlg(Trim(msg + ' ' + S_ENTER_NEW_KEY), mtError, mbYesNoCancel, 0) = ID_YES then
2 daniel-mar 336
        begin
337
          QueryAPIKey;
338
          doRetry := true;
339
        end
340
        else
341
        begin
17 daniel-mar 342
          if not FileExists(cacheFileName) then
343
          begin
344
            raise Exception.Create(S_NO_CACHE_EXISTING);
345
          end;
2 daniel-mar 346
          json := FileGetContents(cacheFileName);
347
          doRetry := false;
348
        end;
349
      end;
350
    end
351
    else // if not FallBackToCache then
352
    begin
353
      if not InteractiveAPIKeyInput then
354
      begin
355
        raise EVtsCurConvException.Create(msg);
356
      end
357
      else
358
      begin
359
        QueryAPIKey(msg);
360
        doRetry := true;
361
      end;
362
    end;
363
  end;
364
 
17 daniel-mar 365
  function protocol: string;
366
  begin
367
    if Secure then result := 'https' else result := 'http';
368
  end;
369
 
370
  function url: string;
371
  var
372
    sDate: string;
373
  begin
374
    if HistoricDate = 0 then
375
    begin
376
      sDate := '';
377
      result := protocol + '://www.apilayer.net/api/live?access_key=' + ReadAPIKey;
378
    end
379
    else
380
    begin
381
      DateTimeToString(sDate, 'YYYY-MM-DD', HistoricDate);
382
      result := protocol + '://www.apilayer.net/api/historical?date=' + sDate + '&access_key=' + ReadAPIKey;
383
    end;
384
  end;
385
 
386
  function cacheFileName: string;
387
  resourcestring
388
    S_CANNOT_CREATE_DIR = 'Cannot create directory %s';
389
  var
390
    sDate: string;
391
    cacheDirName: string;
392
  begin
393
    // cacheDirName := IncludeTrailingPathDelimiter(GetSpecialPath(CSIDL_PROGRAM_FILES_COMMON)) + 'ViaThinkSoft\CurrencyConverter\';
394
    cacheDirName := IncludeTrailingPathDelimiter(GetTempDir) + 'ViaThinkSoft\CurrencyConverter\';
395
    if not ForceDirectories(cacheDirName) then
396
    begin
397
      raise EVtsCurConvException.CreateFmt(S_CANNOT_CREATE_DIR, [cacheDirName]);
398
    end;
399
 
400
    if HistoricDate = 0 then
401
    begin
402
      sDate := '';
403
      result := IncludeTrailingPathDelimiter(cacheDirName) + 'live.json';
404
    end
405
    else
406
    begin
407
      DateTimeToString(sDate, 'YYYY-MM-DD', HistoricDate);
408
      result := IncludeTrailingPathDelimiter(cacheDirName) + 'historical-' + sDate + '.json';
409
    end;
410
  end;
411
 
2 daniel-mar 412
var
17 daniel-mar 413
  sJSON, msg: string;
2 daniel-mar 414
  xRoot: TlkJSONobject;
415
  xSuccess: TlkJSONboolean;
416
  keyInvalid, doRetry: boolean;
417
  needDownload: boolean;
418
  mTime: TDateTime;
13 daniel-mar 419
resourcestring
420
  S_INVALID_MAXAGE = 'Invalid maxage';
421
  S_NO_API_KEY_PROVIDED = 'No API key provided.';
422
  S_DOWNLOAD_QUERY = 'Download %s to %s ?';
423
  S_JSON_FILE_INVALID = 'JSON file invalid';
424
  S_UNKNOWN_SUCCESS = 'Cannot determinate status of the query.';
425
  S_JSON_UNKNOWN_ERROR = 'Unknown error while loading JSON.';
426
  S_API_KEY_INVALID = 'API key invalid.';
2 daniel-mar 427
begin
19 daniel-mar 428
  {$REGION 'Determinate if we need to download or not'}
429
  if HistoricDate = 0 then
430
  begin
431
    needDownload := true;
432
    if MaxAgeSeconds < -1 then
2 daniel-mar 433
    begin
19 daniel-mar 434
      raise EVtsCurConvException.Create(S_INVALID_MAXAGE);
435
    end
436
    else if MaxAgeSeconds = -1 then
437
    begin
438
      // Only download once
439
      needDownload := not FileExists(cacheFileName);
440
    end
441
    else if MaxAgeSeconds = 0 then
442
    begin
443
      // Always download
2 daniel-mar 444
      needDownload := true;
445
    end
19 daniel-mar 446
    else if MaxAgeSeconds > 0 then
2 daniel-mar 447
    begin
19 daniel-mar 448
      // Download if older than <MaxAge> seconds
449
      FileAge(cacheFileName, mTime);
450
      needDownload := not FileExists(cacheFileName) or (SecondsBetween(Now, mTime) > MaxAgeSeconds);
2 daniel-mar 451
    end;
19 daniel-mar 452
  end
453
  else
454
  begin
455
    needDownload := not FileExists(cacheFileName)
456
  end;
457
  {$ENDREGION}
2 daniel-mar 458
 
19 daniel-mar 459
  if not needDownload and FileExists(cacheFileName) then
460
  begin
461
    sJSON := FileGetContents(cacheFileName);
462
  end
463
  else
464
  begin
465
    doRetry := false;
466
 
467
    {$REGION 'Is an API key available?'}
468
    if ReadAPIKey = '' then
2 daniel-mar 469
    begin
19 daniel-mar 470
      _HandleKeyInvalidOrMissing(cacheFileName, S_NO_API_KEY_PROVIDED, doRetry, sJSON);
471
      if not doRetry then
472
      begin
473
        result := sJSON;
474
        Exit;
475
      end;
476
    end;
477
    {$ENDREGION}
2 daniel-mar 478
 
19 daniel-mar 479
    {$REGION 'Download and check if everything is OK'}
480
    repeat
481
      {$REGION 'Confirm web access?'}
482
      if ConfirmWebAccess and (MessageDlg(Format(S_DOWNLOAD_QUERY, [url, cacheFileName]), mtConfirmation, mbYesNoCancel, 0) <> ID_YES) then
2 daniel-mar 483
      begin
19 daniel-mar 484
        if FallBackToCache and FileExists(cacheFileName) then
2 daniel-mar 485
        begin
19 daniel-mar 486
          result := FileGetContents(cacheFileName);
2 daniel-mar 487
          Exit;
19 daniel-mar 488
        end
489
        else Abort;
2 daniel-mar 490
      end;
491
      {$ENDREGION}
492
 
19 daniel-mar 493
      doRetry := false;
2 daniel-mar 494
 
19 daniel-mar 495
      sJSON := GetPage(url);
2 daniel-mar 496
 
19 daniel-mar 497
      xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
498
      if not assigned(xRoot) then raise EVtsCurConvException.Create(S_JSON_FILE_INVALID);
499
      try
2 daniel-mar 500
        xSuccess := xRoot.Field['success'] as TlkJSONboolean;
13 daniel-mar 501
        if not assigned(xSuccess) then raise EVtsCurConvException.Create(S_UNKNOWN_SUCCESS);
2 daniel-mar 502
 
503
        if xSuccess.Value then
504
        begin
505
          try
506
            FilePutContents(cacheFileName, sJSON);
507
          except
508
            // Since this is only a cache, we should not break the whole process if only the saving fails
509
          end;
510
        end
511
        else
512
        begin
513
          {$REGION 'Get information of the error'}
514
          try
515
            keyInvalid := xRoot.Field['error'].Field['code'].Value = 101;
516
            msg := Format('%s (%s, %s)', [
517
              xRoot.Field['error'].Field['info'].Value,
518
              xRoot.Field['error'].Field['code'].Value,
519
              xRoot.Field['error'].Field['type'].Value]);
520
          except
521
            keyInvalid := false;
13 daniel-mar 522
            msg := S_JSON_UNKNOWN_ERROR;
2 daniel-mar 523
          end;
524
          {$ENDREGION}
525
 
526
          if keyInvalid then
527
          begin
13 daniel-mar 528
            _HandleKeyInvalidOrMissing(cacheFileName, S_API_KEY_INVALID, doRetry, sJSON);
2 daniel-mar 529
          end
530
          else // if not keyInvalid then
531
          begin
17 daniel-mar 532
            if FallBackToCache and FileExists(cacheFileName) then
2 daniel-mar 533
            begin
534
              result := FileGetContents(cacheFileName);
535
              Exit;
536
            end
537
            else
538
            begin
539
              raise EVtsCurConvException.Create(msg);
540
            end;
541
          end;
542
        end;
19 daniel-mar 543
      finally
544
        FreeAndNil(xRoot);
545
      end;
546
    until not doRetry;
547
    {$ENDREGION}
548
  end;
2 daniel-mar 549
 
19 daniel-mar 550
  result := sJSON;
22 daniel-mar 551
 
552
  // "USDUSD" missing 06 Aug 2022. A bug?? Reported https://github.com/apilayer/currencylayer-API/issues/16
553
  if Pos('"USDUSD"', result) = 0 then
23 daniel-mar 554
    result := StringReplace(result, '"quotes":{', '"quotes":{"USDUSD":1,', []);
2 daniel-mar 555
end;
556
 
557
end.