Subversion Repositories currency_converter

Rev

Rev 13 | Rev 17 | Go to most recent revision | 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
6
  SysUtils, Classes, Controls;
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?';
2 daniel-mar 320
  begin
321
    if FallBackToCache then
322
    begin
323
      if not InteractiveAPIKeyInput then
324
      begin
325
        json := FileGetContents(cacheFileName);
326
        doRetry := false;
327
      end
328
      else
329
      begin
13 daniel-mar 330
        if MessageDlg(Trim(msg + ' ' + S_ENTER_NEW_KEY), mtError, mbYesNoCancel, 0) = ID_YES then
2 daniel-mar 331
        begin
332
          QueryAPIKey;
333
          doRetry := true;
334
        end
335
        else
336
        begin
337
          json := FileGetContents(cacheFileName);
338
          doRetry := false;
339
        end;
340
      end;
341
    end
342
    else // if not FallBackToCache then
343
    begin
344
      if not InteractiveAPIKeyInput then
345
      begin
346
        raise EVtsCurConvException.Create(msg);
347
      end
348
      else
349
      begin
350
        QueryAPIKey(msg);
351
        doRetry := true;
352
      end;
353
    end;
354
  end;
355
 
356
var
357
  sJSON, msg, protocol: string;
358
  xRoot: TlkJSONobject;
359
  xSuccess: TlkJSONboolean;
360
  keyInvalid, doRetry: boolean;
361
  sDate: string;
362
  url: string;
363
  cacheDirName, cacheFileName: string;
364
  needDownload: boolean;
365
  mTime: TDateTime;
13 daniel-mar 366
resourcestring
367
  S_CANNOT_CREATE_DIR = 'Cannot create directory %s';
368
  S_INVALID_MAXAGE = 'Invalid maxage';
369
  S_NO_API_KEY_PROVIDED = 'No API key provided.';
370
  S_DOWNLOAD_QUERY = 'Download %s to %s ?';
371
  S_JSON_FILE_INVALID = 'JSON file invalid';
372
  S_UNKNOWN_SUCCESS = 'Cannot determinate status of the query.';
373
  S_JSON_UNKNOWN_ERROR = 'Unknown error while loading JSON.';
374
  S_API_KEY_INVALID = 'API key invalid.';
2 daniel-mar 375
begin
376
  try
377
    {$REGION 'Determinate file location and URL'}
378
    // cacheDirName := IncludeTrailingPathDelimiter(GetSpecialPath(CSIDL_PROGRAM_FILES_COMMON)) + 'ViaThinkSoft\CurrencyConverter\';
379
    cacheDirName := IncludeTrailingPathDelimiter(GetTempDir) + 'ViaThinkSoft\CurrencyConverter\';
380
    if not ForceDirectories(cacheDirName) then
381
    begin
13 daniel-mar 382
      raise EVtsCurConvException.CreateFmt(S_CANNOT_CREATE_DIR, [cacheDirName]);
2 daniel-mar 383
    end;
384
 
385
    if Secure then protocol := 'https' else protocol := 'http';
386
    if HistoricDate = 0 then
387
    begin
388
      sDate := '';
389
      url := protocol + '://www.apilayer.net/api/live?access_key=' + ReadAPIKey;
390
      cacheFileName := IncludeTrailingPathDelimiter(cacheDirName) + 'live.json';
391
    end
392
    else
393
    begin
394
      DateTimeToString(sDate, 'YYYY-MM-DD', HistoricDate);
395
      url := protocol + '://www.apilayer.net/api/historical?date=' + sDate + '&access_key=' + ReadAPIKey;
396
      cacheFileName := IncludeTrailingPathDelimiter(cacheDirName) + 'historical-' + sDate + '.json';
397
    end;
398
    {$ENDREGION}
399
 
400
    {$REGION 'Determinate if we need to download or not'}
401
    if HistoricDate = 0 then
402
    begin
403
      needDownload := true;
404
      if MaxAgeSeconds < -1 then
405
      begin
13 daniel-mar 406
        raise EVtsCurConvException.Create(S_INVALID_MAXAGE);
2 daniel-mar 407
      end
408
      else if MaxAgeSeconds = -1 then
409
      begin
410
        // Only download once
411
        needDownload := not FileExists(cacheFileName);
412
      end
413
      else if MaxAgeSeconds = 0 then
414
      begin
415
        // Always download
416
        needDownload := true;
417
      end
418
      else if MaxAgeSeconds > 0 then
419
      begin
420
        // Download if older than <MaxAge> seconds
421
        FileAge(cacheFileName, mTime);
422
        needDownload := not FileExists(cacheFileName) or (SecondsBetween(Now, mTime) > MaxAgeSeconds);
423
      end;
424
    end
425
    else
426
    begin
427
      needDownload := not FileExists(cacheFileName)
428
    end;
429
    {$ENDREGION}
430
 
431
    if not needDownload then
432
    begin
433
      sJSON := FileGetContents(cacheFileName);
434
    end
435
    else
436
    begin
437
      doRetry := false;
438
 
439
      {$REGION 'Is an API key available?'}
440
      if ReadAPIKey = '' then
441
      begin
13 daniel-mar 442
        _HandleKeyInvalidOrMissing(cacheFileName, S_NO_API_KEY_PROVIDED, doRetry, sJSON);
2 daniel-mar 443
        if not doRetry then
444
        begin
445
          result := sJSON;
446
          Exit;
447
        end;
448
      end;
449
      {$ENDREGION}
450
 
451
      {$REGION 'Download and check if everything is OK'}
452
      repeat
453
        {$REGION 'Confirm web access?'}
13 daniel-mar 454
        if ConfirmWebAccess and (MessageDlg(Format(S_DOWNLOAD_QUERY, [url, cacheFileName]), mtConfirmation, mbYesNoCancel, 0) <> ID_YES) then
2 daniel-mar 455
        begin
456
          if FallBackToCache then
457
          begin
458
            result := FileGetContents(cacheFileName);
459
            Exit;
460
          end
461
          else Abort;
462
        end;
463
        {$ENDREGION}
464
 
465
        doRetry := false;
466
 
467
        sJSON := GetPage(url);
468
 
469
        xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
13 daniel-mar 470
        if not assigned(xRoot) then raise EVtsCurConvException.Create(S_JSON_FILE_INVALID);
2 daniel-mar 471
 
472
        xSuccess := xRoot.Field['success'] as TlkJSONboolean;
13 daniel-mar 473
        if not assigned(xSuccess) then raise EVtsCurConvException.Create(S_UNKNOWN_SUCCESS);
2 daniel-mar 474
 
475
        if xSuccess.Value then
476
        begin
477
          try
478
            FilePutContents(cacheFileName, sJSON);
479
          except
480
            // Since this is only a cache, we should not break the whole process if only the saving fails
481
          end;
482
        end
483
        else
484
        begin
485
          {$REGION 'Get information of the error'}
486
          try
487
            keyInvalid := xRoot.Field['error'].Field['code'].Value = 101;
488
            msg := Format('%s (%s, %s)', [
489
              xRoot.Field['error'].Field['info'].Value,
490
              xRoot.Field['error'].Field['code'].Value,
491
              xRoot.Field['error'].Field['type'].Value]);
492
          except
493
            keyInvalid := false;
13 daniel-mar 494
            msg := S_JSON_UNKNOWN_ERROR;
2 daniel-mar 495
          end;
496
          {$ENDREGION}
497
 
498
          if keyInvalid then
499
          begin
13 daniel-mar 500
            _HandleKeyInvalidOrMissing(cacheFileName, S_API_KEY_INVALID, doRetry, sJSON);
2 daniel-mar 501
          end
502
          else // if not keyInvalid then
503
          begin
504
            if FallBackToCache then
505
            begin
506
              result := FileGetContents(cacheFileName);
507
              Exit;
508
            end
509
            else
510
            begin
511
              raise EVtsCurConvException.Create(msg);
512
            end;
513
          end;
514
        end;
515
      until not doRetry;
516
      {$ENDREGION}
517
    end;
518
 
519
    result := sJSON;
520
  finally
521
    FreeAndNil(xRoot);
522
  end;
523
end;
524
 
525
end.