Subversion Repositories currency_converter

Rev

Rev 2 | Rev 16 | 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));
211
 
212
  sJSON := GetJsonRaw(HistoricDate);
213
 
214
  xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
215
  try
216
    xSource := xRoot.Field['source'] as TlkJSONstring;
13 daniel-mar 217
    if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
2 daniel-mar 218
 
219
    xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
13 daniel-mar 220
    if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
2 daniel-mar 221
 
222
    rateToFound := false;
223
    rateFromFound := false;
224
    rateTo := 0.00; // to avoid that the compiler shows a warning
225
    rateFrom := 0.00; // to avoid that the compiler shows a warning
226
 
227
    for i := 0 to xQuotes.Count - 1 do
228
    begin
13 daniel-mar 229
      if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
2 daniel-mar 230
 
231
      xRate := xQuotes.Field[xQuotes.NameOf[i]] as TlkJSONnumber;
13 daniel-mar 232
      if not Assigned(xRate) then raise EVtsCurConvException.Create(S_JSON_RATE_MISSING);
2 daniel-mar 233
 
234
      if Copy(xQuotes.NameOf[i], 1, 3) = xSource.Value then
235
      begin
236
        if Copy(xQuotes.NameOf[i], 4, 3) = toCur then
237
        begin
238
          rateTo := xRate.Value;
239
          rateToFound := true;
240
        end;
241
        if Copy(xQuotes.NameOf[i], 4, 3) = fromCur then
242
        begin
243
          rateFrom := xRate.Value;
244
          rateFromFound := true;
245
        end;
246
      end;
247
    end;
248
 
13 daniel-mar 249
    if not rateToFound then raise EVtsCurConvException.CreateFmt(S_CURRENCY_NOT_SUPPORTED, [toCur]);
250
    if not rateFromFound then raise EVtsCurConvException.CreateFmt(S_CURRENCY_NOT_SUPPORTED, [fromCur]);
2 daniel-mar 251
 
252
    result := value * rateTo / rateFrom;
253
  finally
254
    xRoot.Free;
255
  end;
256
end;
257
 
258
procedure TVtsCurConv.QueryAPIKey(msg: string='');
259
var
260
  s: string;
13 daniel-mar 261
resourcestring
262
  S_CURRENCYLAYER = 'currencylayer.com';
263
  S_ENTER_KEY = 'Please enter your API key:';
264
  S_NO_API_KEY = 'No API key provided.';
2 daniel-mar 265
begin
13 daniel-mar 266
  s := Trim(InputBox(S_CURRENCYLAYER, Trim(msg + ' ' + S_ENTER_KEY), ''));
267
  if s = '' then raise EVtsCurConvException.Create(S_NO_API_KEY);
2 daniel-mar 268
  WriteAPIKey(s);
269
end;
270
 
271
function TVtsCurConv.GetAcceptedCurrencies(sl: TStringList=nil; HistoricDate: TDate=0): integer;
272
var
273
  i: Integer;
274
  sJSON: String;
275
  xSource: TlkJSONstring;
276
  xRoot: TlkJSONobject;
277
  xQuotes: TlkJSONobject;
13 daniel-mar 278
resourcestring
279
  S_JSON_ENTRY_MISSING = 'JSON entry "%s" is missing!';
280
  S_WRONG_QUOTE_LEN = 'Length of quotes-entry is unexpected!';
2 daniel-mar 281
begin
282
  result := 0;
283
 
284
  sJSON := GetJsonRaw(HistoricDate);
285
 
286
  xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
287
  try
288
    xSource := xRoot.Field['source'] as TlkJSONstring;
13 daniel-mar 289
    if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
2 daniel-mar 290
 
291
    xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
13 daniel-mar 292
    if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
2 daniel-mar 293
 
294
    for i := 0 to xQuotes.Count - 1 do
295
    begin
13 daniel-mar 296
      if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
2 daniel-mar 297
 
298
      if Copy(xQuotes.NameOf[i], 1, 3) = xSource.Value then
299
      begin
300
        Inc(result);
301
        if Assigned(sl) then sl.Add(Copy(xQuotes.NameOf[i], 4, 3));
302
      end;
303
    end;
304
  finally
305
    xRoot.Free;
306
  end;
307
end;
308
 
309
function TVtsCurConv.GetJsonRaw(HistoricDate: TDate=0): string;
310
 
311
  procedure _HandleKeyInvalidOrMissing(cacheFileName: string; msg: string; out doRetry: boolean; out json: string);
13 daniel-mar 312
  resourcestring
313
    S_ENTER_NEW_KEY = 'Do you want to enter a new one?';
2 daniel-mar 314
  begin
315
    if FallBackToCache then
316
    begin
317
      if not InteractiveAPIKeyInput then
318
      begin
319
        json := FileGetContents(cacheFileName);
320
        doRetry := false;
321
      end
322
      else
323
      begin
13 daniel-mar 324
        if MessageDlg(Trim(msg + ' ' + S_ENTER_NEW_KEY), mtError, mbYesNoCancel, 0) = ID_YES then
2 daniel-mar 325
        begin
326
          QueryAPIKey;
327
          doRetry := true;
328
        end
329
        else
330
        begin
331
          json := FileGetContents(cacheFileName);
332
          doRetry := false;
333
        end;
334
      end;
335
    end
336
    else // if not FallBackToCache then
337
    begin
338
      if not InteractiveAPIKeyInput then
339
      begin
340
        raise EVtsCurConvException.Create(msg);
341
      end
342
      else
343
      begin
344
        QueryAPIKey(msg);
345
        doRetry := true;
346
      end;
347
    end;
348
  end;
349
 
350
var
351
  sJSON, msg, protocol: string;
352
  xRoot: TlkJSONobject;
353
  xSuccess: TlkJSONboolean;
354
  keyInvalid, doRetry: boolean;
355
  sDate: string;
356
  url: string;
357
  cacheDirName, cacheFileName: string;
358
  needDownload: boolean;
359
  mTime: TDateTime;
13 daniel-mar 360
resourcestring
361
  S_CANNOT_CREATE_DIR = 'Cannot create directory %s';
362
  S_INVALID_MAXAGE = 'Invalid maxage';
363
  S_NO_API_KEY_PROVIDED = 'No API key provided.';
364
  S_DOWNLOAD_QUERY = 'Download %s to %s ?';
365
  S_JSON_FILE_INVALID = 'JSON file invalid';
366
  S_UNKNOWN_SUCCESS = 'Cannot determinate status of the query.';
367
  S_JSON_UNKNOWN_ERROR = 'Unknown error while loading JSON.';
368
  S_API_KEY_INVALID = 'API key invalid.';
2 daniel-mar 369
begin
370
  try
371
    {$REGION 'Determinate file location and URL'}
372
    // cacheDirName := IncludeTrailingPathDelimiter(GetSpecialPath(CSIDL_PROGRAM_FILES_COMMON)) + 'ViaThinkSoft\CurrencyConverter\';
373
    cacheDirName := IncludeTrailingPathDelimiter(GetTempDir) + 'ViaThinkSoft\CurrencyConverter\';
374
    if not ForceDirectories(cacheDirName) then
375
    begin
13 daniel-mar 376
      raise EVtsCurConvException.CreateFmt(S_CANNOT_CREATE_DIR, [cacheDirName]);
2 daniel-mar 377
    end;
378
 
379
    if Secure then protocol := 'https' else protocol := 'http';
380
    if HistoricDate = 0 then
381
    begin
382
      sDate := '';
383
      url := protocol + '://www.apilayer.net/api/live?access_key=' + ReadAPIKey;
384
      cacheFileName := IncludeTrailingPathDelimiter(cacheDirName) + 'live.json';
385
    end
386
    else
387
    begin
388
      DateTimeToString(sDate, 'YYYY-MM-DD', HistoricDate);
389
      url := protocol + '://www.apilayer.net/api/historical?date=' + sDate + '&access_key=' + ReadAPIKey;
390
      cacheFileName := IncludeTrailingPathDelimiter(cacheDirName) + 'historical-' + sDate + '.json';
391
    end;
392
    {$ENDREGION}
393
 
394
    {$REGION 'Determinate if we need to download or not'}
395
    if HistoricDate = 0 then
396
    begin
397
      needDownload := true;
398
      if MaxAgeSeconds < -1 then
399
      begin
13 daniel-mar 400
        raise EVtsCurConvException.Create(S_INVALID_MAXAGE);
2 daniel-mar 401
      end
402
      else if MaxAgeSeconds = -1 then
403
      begin
404
        // Only download once
405
        needDownload := not FileExists(cacheFileName);
406
      end
407
      else if MaxAgeSeconds = 0 then
408
      begin
409
        // Always download
410
        needDownload := true;
411
      end
412
      else if MaxAgeSeconds > 0 then
413
      begin
414
        // Download if older than <MaxAge> seconds
415
        FileAge(cacheFileName, mTime);
416
        needDownload := not FileExists(cacheFileName) or (SecondsBetween(Now, mTime) > MaxAgeSeconds);
417
      end;
418
    end
419
    else
420
    begin
421
      needDownload := not FileExists(cacheFileName)
422
    end;
423
    {$ENDREGION}
424
 
425
    if not needDownload then
426
    begin
427
      sJSON := FileGetContents(cacheFileName);
428
    end
429
    else
430
    begin
431
      doRetry := false;
432
 
433
      {$REGION 'Is an API key available?'}
434
      if ReadAPIKey = '' then
435
      begin
13 daniel-mar 436
        _HandleKeyInvalidOrMissing(cacheFileName, S_NO_API_KEY_PROVIDED, doRetry, sJSON);
2 daniel-mar 437
        if not doRetry then
438
        begin
439
          result := sJSON;
440
          Exit;
441
        end;
442
      end;
443
      {$ENDREGION}
444
 
445
      {$REGION 'Download and check if everything is OK'}
446
      repeat
447
        {$REGION 'Confirm web access?'}
13 daniel-mar 448
        if ConfirmWebAccess and (MessageDlg(Format(S_DOWNLOAD_QUERY, [url, cacheFileName]), mtConfirmation, mbYesNoCancel, 0) <> ID_YES) then
2 daniel-mar 449
        begin
450
          if FallBackToCache then
451
          begin
452
            result := FileGetContents(cacheFileName);
453
            Exit;
454
          end
455
          else Abort;
456
        end;
457
        {$ENDREGION}
458
 
459
        doRetry := false;
460
 
461
        sJSON := GetPage(url);
462
 
463
        xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
13 daniel-mar 464
        if not assigned(xRoot) then raise EVtsCurConvException.Create(S_JSON_FILE_INVALID);
2 daniel-mar 465
 
466
        xSuccess := xRoot.Field['success'] as TlkJSONboolean;
13 daniel-mar 467
        if not assigned(xSuccess) then raise EVtsCurConvException.Create(S_UNKNOWN_SUCCESS);
2 daniel-mar 468
 
469
        if xSuccess.Value then
470
        begin
471
          try
472
            FilePutContents(cacheFileName, sJSON);
473
          except
474
            // Since this is only a cache, we should not break the whole process if only the saving fails
475
          end;
476
        end
477
        else
478
        begin
479
          {$REGION 'Get information of the error'}
480
          try
481
            keyInvalid := xRoot.Field['error'].Field['code'].Value = 101;
482
            msg := Format('%s (%s, %s)', [
483
              xRoot.Field['error'].Field['info'].Value,
484
              xRoot.Field['error'].Field['code'].Value,
485
              xRoot.Field['error'].Field['type'].Value]);
486
          except
487
            keyInvalid := false;
13 daniel-mar 488
            msg := S_JSON_UNKNOWN_ERROR;
2 daniel-mar 489
          end;
490
          {$ENDREGION}
491
 
492
          if keyInvalid then
493
          begin
13 daniel-mar 494
            _HandleKeyInvalidOrMissing(cacheFileName, S_API_KEY_INVALID, doRetry, sJSON);
2 daniel-mar 495
          end
496
          else // if not keyInvalid then
497
          begin
498
            if FallBackToCache then
499
            begin
500
              result := FileGetContents(cacheFileName);
501
              Exit;
502
            end
503
            else
504
            begin
505
              raise EVtsCurConvException.Create(msg);
506
            end;
507
          end;
508
        end;
509
      until not doRetry;
510
      {$ENDREGION}
511
    end;
512
 
513
    result := sJSON;
514
  finally
515
    FreeAndNil(xRoot);
516
  end;
517
end;
518
 
519
end.