Subversion Repositories currency_converter

Rev

Rev 13 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  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.
  493.