Subversion Repositories currency_converter

Rev

Rev 13 | Rev 17 | 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. resourcestring
  76.   S_CANNOT_DOWNLOAD = 'Cannot download from %s';
  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
  88.         raise EVtsCurConvException.CreateFmt(S_CANNOT_DOWNLOAD, [aURL]);
  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;
  119.   resourcestring
  120.     S_CANNOT_OPEN_REGISTRY = 'Cannot open registry key';
  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
  130.       else raise EVtsCurConvException.Create(S_CANNOT_OPEN_REGISTRY);
  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;
  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';
  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.   if fromCur = toCur then
  213.   begin
  214.     result := value;
  215.     exit;
  216.   end;
  217.  
  218.   sJSON := GetJsonRaw(HistoricDate);
  219.  
  220.   xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
  221.   try
  222.     xSource := xRoot.Field['source'] as TlkJSONstring;
  223.     if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
  224.  
  225.     xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
  226.     if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
  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
  235.       if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
  236.  
  237.       xRate := xQuotes.Field[xQuotes.NameOf[i]] as TlkJSONnumber;
  238.       if not Assigned(xRate) then raise EVtsCurConvException.Create(S_JSON_RATE_MISSING);
  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.  
  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]);
  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;
  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.';
  271. begin
  272.   s := Trim(InputBox(S_CURRENCYLAYER, Trim(msg + ' ' + S_ENTER_KEY), ''));
  273.   if s = '' then raise EVtsCurConvException.Create(S_NO_API_KEY);
  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;
  284. resourcestring
  285.   S_JSON_ENTRY_MISSING = 'JSON entry "%s" is missing!';
  286.   S_WRONG_QUOTE_LEN = 'Length of quotes-entry is unexpected!';
  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;
  295.     if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
  296.  
  297.     xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
  298.     if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
  299.  
  300.     for i := 0 to xQuotes.Count - 1 do
  301.     begin
  302.       if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
  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);
  318.   resourcestring
  319.     S_ENTER_NEW_KEY = 'Do you want to enter a new one?';
  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
  330.         if MessageDlg(Trim(msg + ' ' + S_ENTER_NEW_KEY), mtError, mbYesNoCancel, 0) = ID_YES then
  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;
  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.';
  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
  382.       raise EVtsCurConvException.CreateFmt(S_CANNOT_CREATE_DIR, [cacheDirName]);
  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
  406.         raise EVtsCurConvException.Create(S_INVALID_MAXAGE);
  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
  442.         _HandleKeyInvalidOrMissing(cacheFileName, S_NO_API_KEY_PROVIDED, doRetry, sJSON);
  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?'}
  454.         if ConfirmWebAccess and (MessageDlg(Format(S_DOWNLOAD_QUERY, [url, cacheFileName]), mtConfirmation, mbYesNoCancel, 0) <> ID_YES) then
  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;
  470.         if not assigned(xRoot) then raise EVtsCurConvException.Create(S_JSON_FILE_INVALID);
  471.  
  472.         xSuccess := xRoot.Field['success'] as TlkJSONboolean;
  473.         if not assigned(xSuccess) then raise EVtsCurConvException.Create(S_UNKNOWN_SUCCESS);
  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;
  494.             msg := S_JSON_UNKNOWN_ERROR;
  495.           end;
  496.           {$ENDREGION}
  497.  
  498.           if keyInvalid then
  499.           begin
  500.             _HandleKeyInvalidOrMissing(cacheFileName, S_API_KEY_INVALID, doRetry, sJSON);
  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.
  526.