Subversion Repositories currency_converter

Rev

Rev 13 | Go to most recent revision | Details | 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;
75
begin
76
  // https://stackoverflow.com/questions/9239267/how-to-download-a-web-page-into-a-variable
77
  Result := '';
78
  Response := TStringStream.Create('');
79
  try
80
    HTTP := TIdHTTP.Create(nil);
81
    try
82
      HTTP.Get(aURL, Response);
83
      if HTTP.ResponseCode = HTTP_RESPONSE_OK then
84
        Result := Response.DataString
85
      else
86
        raise EVtsCurConvException.CreateFmt('Cannot download from %s', [aURL]);
87
    finally
88
      HTTP.Free;
89
    end;
90
  finally
91
    Response.Free;
92
  end;
93
end;
94
 
95
function GetTempDir: string;
96
var
97
  Dir: string;
98
  Len: DWord;
99
begin
100
  SetLength(Dir, MAX_PATH);
101
  Len := GetTempPath(MAX_PATH, PChar(Dir));
102
  if Len > 0 then
103
  begin
104
    SetLength(Dir, Len);
105
    Result := Dir;
106
  end
107
  else
108
    RaiseLastOSError;
109
end;
110
 
111
{ TVtsCurConv }
112
 
113
class procedure TVtsCurConv.WriteAPIKey(key: TVtsCurApiKey; UserMode: boolean=true);
114
  procedure _WriteAPIKey(root: HKEY);
115
  var
116
    reg: TRegistry;
117
  begin
118
    reg := TRegistry.Create;
119
    try
120
      reg.RootKey := root;
121
      if reg.OpenKey('Software\ViaThinkSoft\CurrencyConverter', true) then
122
      begin
123
        reg.WriteString('APIKey', key);
124
        reg.CloseKey;
125
      end
126
      else raise EVtsCurConvException.Create('Cannot open registry key');
127
    finally
128
      reg.Free;
129
    end;
130
  end;
131
begin
132
  if UserMode then
133
    _WriteAPIKey(HKEY_CURRENT_USER)
134
  else
135
    _WriteAPIKey(HKEY_LOCAL_MACHINE);
136
end;
137
 
138
class function TVtsCurConv.DeleteAPIKey(UserMode: boolean=true): boolean;
139
  procedure _DeleteAPIKey(root: HKEY);
140
  var
141
    reg: TRegistry;
142
  begin
143
    reg := TRegistry.Create;
144
    try
145
      reg.RootKey := root;
146
      if reg.OpenKey('Software\ViaThinkSoft\CurrencyConverter', true) then
147
      begin
148
        result := reg.DeleteValue('APIKey');
149
        reg.CloseKey;
150
      end;
151
    finally
152
      reg.Free;
153
    end;
154
  end;
155
begin
156
  result := false;
157
  if UserMode then
158
    _DeleteAPIKey(HKEY_CURRENT_USER)
159
  else
160
    _DeleteAPIKey(HKEY_LOCAL_MACHINE);
161
end;
162
 
163
class function TVtsCurConv.ReadAPIKey: TVtsCurApiKey;
164
  function _ReadAPIKey(root: HKEY): string;
165
  var
166
    reg: TRegistry;
167
  begin
168
    result := '';
169
    reg := TRegistry.Create;
170
    try
171
      reg.RootKey := root;
172
      if reg.OpenKeyReadOnly('Software\ViaThinkSoft\CurrencyConverter') then
173
      begin
174
        if reg.ValueExists('APIKey') then result := reg.ReadString('APIKey');
175
        reg.CloseKey;
176
      end;
177
    finally
178
      reg.Free;
179
    end;
180
  end;
181
begin
182
  result := _ReadAPIKey(HKEY_CURRENT_USER);
183
  if result = '' then result := _ReadAPIKey(HKEY_LOCAL_MACHINE);
184
end;
185
 
186
function TVtsCurConv.Convert(value: Currency; fromCur, toCur: TVtsCur; HistoricDate: TDate=0): Currency;
187
var
188
  rateTo, rateFrom: TVtsRate;
189
  i: Integer;
190
  rateToFound: Boolean;
191
  rateFromFound: Boolean;
192
  sJSON: String;
193
  xSource: TlkJSONstring;
194
  xRoot: TlkJSONobject;
195
  xQuotes: TlkJSONobject;
196
  xRate: TlkJSONnumber;
197
begin
198
  result := 0; // to avoid that the compiler shows a warning
199
 
200
  fromCur := Trim(UpperCase(fromCur));
201
  toCur := Trim(UpperCase(toCur));
202
 
203
  sJSON := GetJsonRaw(HistoricDate);
204
 
205
  xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
206
  try
207
    xSource := xRoot.Field['source'] as TlkJSONstring;
208
    if not assigned(xSource) then raise EVtsCurConvException.Create('JSON entry "source" is missing!');
209
 
210
    xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
211
    if not assigned(xQuotes) then raise EVtsCurConvException.Create('JSON entry "quotes" is missing!');
212
 
213
    rateToFound := false;
214
    rateFromFound := false;
215
    rateTo := 0.00; // to avoid that the compiler shows a warning
216
    rateFrom := 0.00; // to avoid that the compiler shows a warning
217
 
218
    for i := 0 to xQuotes.Count - 1 do
219
    begin
220
      if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create('Length of quotes-entry is unexpected!');
221
 
222
      xRate := xQuotes.Field[xQuotes.NameOf[i]] as TlkJSONnumber;
223
      if not Assigned(xRate) then raise EVtsCurConvException.Create('JSON entry quotes->rate is missing!');
224
 
225
      if Copy(xQuotes.NameOf[i], 1, 3) = xSource.Value then
226
      begin
227
        if Copy(xQuotes.NameOf[i], 4, 3) = toCur then
228
        begin
229
          rateTo := xRate.Value;
230
          rateToFound := true;
231
        end;
232
        if Copy(xQuotes.NameOf[i], 4, 3) = fromCur then
233
        begin
234
          rateFrom := xRate.Value;
235
          rateFromFound := true;
236
        end;
237
      end;
238
    end;
239
 
240
    if not rateToFound then raise EVtsCurConvException.CreateFmt('Currency "%s" not supported', [toCur]);
241
    if not rateFromFound then raise EVtsCurConvException.CreateFmt('Currency "%s" not supported', [fromCur]);
242
 
243
    result := value * rateTo / rateFrom;
244
  finally
245
    xRoot.Free;
246
  end;
247
end;
248
 
249
procedure TVtsCurConv.QueryAPIKey(msg: string='');
250
var
251
  s: string;
252
begin
253
  s := Trim(InputBox('currencylayer.com', Trim(msg + ' Please enter your API key:'), ''));
254
  if s = '' then raise EVtsCurConvException.Create('No API key provided.');
255
  WriteAPIKey(s);
256
end;
257
 
258
function TVtsCurConv.GetAcceptedCurrencies(sl: TStringList=nil; HistoricDate: TDate=0): integer;
259
var
260
  i: Integer;
261
  sJSON: String;
262
  xSource: TlkJSONstring;
263
  xRoot: TlkJSONobject;
264
  xQuotes: TlkJSONobject;
265
begin
266
  result := 0;
267
 
268
  sJSON := GetJsonRaw(HistoricDate);
269
 
270
  xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
271
  try
272
    xSource := xRoot.Field['source'] as TlkJSONstring;
273
    if not assigned(xSource) then raise EVtsCurConvException.Create('JSON entry "source" is missing!');
274
 
275
    xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
276
    if not assigned(xQuotes) then raise EVtsCurConvException.Create('JSON entry "quotes" is missing!');
277
 
278
    for i := 0 to xQuotes.Count - 1 do
279
    begin
280
      if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create('Length of quotes-entry is unexpected!');
281
 
282
      if Copy(xQuotes.NameOf[i], 1, 3) = xSource.Value then
283
      begin
284
        Inc(result);
285
        if Assigned(sl) then sl.Add(Copy(xQuotes.NameOf[i], 4, 3));
286
      end;
287
    end;
288
  finally
289
    xRoot.Free;
290
  end;
291
end;
292
 
293
function TVtsCurConv.GetJsonRaw(HistoricDate: TDate=0): string;
294
 
295
  procedure _HandleKeyInvalidOrMissing(cacheFileName: string; msg: string; out doRetry: boolean; out json: string);
296
  begin
297
    if FallBackToCache then
298
    begin
299
      if not InteractiveAPIKeyInput then
300
      begin
301
        json := FileGetContents(cacheFileName);
302
        doRetry := false;
303
      end
304
      else
305
      begin
306
        if MessageDlg(Trim(msg + ' Do you want to enter a new one?'), mtError, mbYesNoCancel, 0) = ID_YES then
307
        begin
308
          QueryAPIKey;
309
          doRetry := true;
310
        end
311
        else
312
        begin
313
          json := FileGetContents(cacheFileName);
314
          doRetry := false;
315
        end;
316
      end;
317
    end
318
    else // if not FallBackToCache then
319
    begin
320
      if not InteractiveAPIKeyInput then
321
      begin
322
        raise EVtsCurConvException.Create(msg);
323
      end
324
      else
325
      begin
326
        QueryAPIKey(msg);
327
        doRetry := true;
328
      end;
329
    end;
330
  end;
331
 
332
var
333
  sJSON, msg, protocol: string;
334
  xRoot: TlkJSONobject;
335
  xSuccess: TlkJSONboolean;
336
  keyInvalid, doRetry: boolean;
337
  sDate: string;
338
  url: string;
339
  cacheDirName, cacheFileName: string;
340
  needDownload: boolean;
341
  mTime: TDateTime;
342
begin
343
  try
344
    {$REGION 'Determinate file location and URL'}
345
    // cacheDirName := IncludeTrailingPathDelimiter(GetSpecialPath(CSIDL_PROGRAM_FILES_COMMON)) + 'ViaThinkSoft\CurrencyConverter\';
346
    cacheDirName := IncludeTrailingPathDelimiter(GetTempDir) + 'ViaThinkSoft\CurrencyConverter\';
347
    if not ForceDirectories(cacheDirName) then
348
    begin
349
      raise EVtsCurConvException.CreateFmt('Cannot create directory %s', [cacheDirName]);
350
    end;
351
 
352
    if Secure then protocol := 'https' else protocol := 'http';
353
    if HistoricDate = 0 then
354
    begin
355
      sDate := '';
356
      url := protocol + '://www.apilayer.net/api/live?access_key=' + ReadAPIKey;
357
      cacheFileName := IncludeTrailingPathDelimiter(cacheDirName) + 'live.json';
358
    end
359
    else
360
    begin
361
      DateTimeToString(sDate, 'YYYY-MM-DD', HistoricDate);
362
      url := protocol + '://www.apilayer.net/api/historical?date=' + sDate + '&access_key=' + ReadAPIKey;
363
      cacheFileName := IncludeTrailingPathDelimiter(cacheDirName) + 'historical-' + sDate + '.json';
364
    end;
365
    {$ENDREGION}
366
 
367
    {$REGION 'Determinate if we need to download or not'}
368
    if HistoricDate = 0 then
369
    begin
370
      needDownload := true;
371
      if MaxAgeSeconds < -1 then
372
      begin
373
        raise EVtsCurConvException.Create('Invalid maxage');
374
      end
375
      else if MaxAgeSeconds = -1 then
376
      begin
377
        // Only download once
378
        needDownload := not FileExists(cacheFileName);
379
      end
380
      else if MaxAgeSeconds = 0 then
381
      begin
382
        // Always download
383
        needDownload := true;
384
      end
385
      else if MaxAgeSeconds > 0 then
386
      begin
387
        // Download if older than <MaxAge> seconds
388
        FileAge(cacheFileName, mTime);
389
        needDownload := not FileExists(cacheFileName) or (SecondsBetween(Now, mTime) > MaxAgeSeconds);
390
      end;
391
    end
392
    else
393
    begin
394
      needDownload := not FileExists(cacheFileName)
395
    end;
396
    {$ENDREGION}
397
 
398
    if not needDownload then
399
    begin
400
      sJSON := FileGetContents(cacheFileName);
401
    end
402
    else
403
    begin
404
      doRetry := false;
405
 
406
      {$REGION 'Is an API key available?'}
407
      if ReadAPIKey = '' then
408
      begin
409
        _HandleKeyInvalidOrMissing(cacheFileName, 'No API key provided.', doRetry, sJSON);
410
        if not doRetry then
411
        begin
412
          result := sJSON;
413
          Exit;
414
        end;
415
      end;
416
      {$ENDREGION}
417
 
418
      {$REGION 'Download and check if everything is OK'}
419
      repeat
420
        {$REGION 'Confirm web access?'}
421
        if ConfirmWebAccess and (MessageDlg('Download ' + url + ' to ' + cacheFileName + ' ?', mtConfirmation, mbYesNoCancel, 0) <> ID_YES) then
422
        begin
423
          if FallBackToCache then
424
          begin
425
            result := FileGetContents(cacheFileName);
426
            Exit;
427
          end
428
          else Abort;
429
        end;
430
        {$ENDREGION}
431
 
432
        doRetry := false;
433
 
434
        sJSON := GetPage(url);
435
 
436
        xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
437
        if not assigned(xRoot) then raise EVtsCurConvException.Create('JSON file invalid');
438
 
439
        xSuccess := xRoot.Field['success'] as TlkJSONboolean;
440
        if not assigned(xSuccess) then raise EVtsCurConvException.Create('Cannot determinate status of the query.');
441
 
442
        if xSuccess.Value then
443
        begin
444
          try
445
            FilePutContents(cacheFileName, sJSON);
446
          except
447
            // Since this is only a cache, we should not break the whole process if only the saving fails
448
          end;
449
        end
450
        else
451
        begin
452
          {$REGION 'Get information of the error'}
453
          try
454
            keyInvalid := xRoot.Field['error'].Field['code'].Value = 101;
455
            msg := Format('%s (%s, %s)', [
456
              xRoot.Field['error'].Field['info'].Value,
457
              xRoot.Field['error'].Field['code'].Value,
458
              xRoot.Field['error'].Field['type'].Value]);
459
          except
460
            keyInvalid := false;
461
            msg := 'Unknown error while loading JSON.';
462
          end;
463
          {$ENDREGION}
464
 
465
          if keyInvalid then
466
          begin
467
            _HandleKeyInvalidOrMissing(cacheFileName, 'API key invalid.', doRetry, sJSON);
468
          end
469
          else // if not keyInvalid then
470
          begin
471
            if FallBackToCache then
472
            begin
473
              result := FileGetContents(cacheFileName);
474
              Exit;
475
            end
476
            else
477
            begin
478
              raise EVtsCurConvException.Create(msg);
479
            end;
480
          end;
481
        end;
482
      until not doRetry;
483
      {$ENDREGION}
484
    end;
485
 
486
    result := sJSON;
487
  finally
488
    FreeAndNil(xRoot);
489
  end;
490
end;
491
 
492
end.