Subversion Repositories currency_converter

Rev

Rev 2 | Rev 16 | 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.   sJSON := GetJsonRaw(HistoricDate);
  213.  
  214.   xRoot := TlkJSON.ParseText(sJSON) as TlkJSONobject;
  215.   try
  216.     xSource := xRoot.Field['source'] as TlkJSONstring;
  217.     if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
  218.  
  219.     xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
  220.     if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
  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
  229.       if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
  230.  
  231.       xRate := xQuotes.Field[xQuotes.NameOf[i]] as TlkJSONnumber;
  232.       if not Assigned(xRate) then raise EVtsCurConvException.Create(S_JSON_RATE_MISSING);
  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.  
  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]);
  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;
  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.';
  265. begin
  266.   s := Trim(InputBox(S_CURRENCYLAYER, Trim(msg + ' ' + S_ENTER_KEY), ''));
  267.   if s = '' then raise EVtsCurConvException.Create(S_NO_API_KEY);
  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;
  278. resourcestring
  279.   S_JSON_ENTRY_MISSING = 'JSON entry "%s" is missing!';
  280.   S_WRONG_QUOTE_LEN = 'Length of quotes-entry is unexpected!';
  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;
  289.     if not assigned(xSource) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['source']);
  290.  
  291.     xQuotes := xRoot.Field['quotes'] as TlkJSONobject;
  292.     if not assigned(xQuotes) then raise EVtsCurConvException.CreateFmt(S_JSON_ENTRY_MISSING, ['quotes']);
  293.  
  294.     for i := 0 to xQuotes.Count - 1 do
  295.     begin
  296.       if Length(xQuotes.NameOf[i]) <> 6 then raise EVtsCurConvException.Create(S_WRONG_QUOTE_LEN);
  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);
  312.   resourcestring
  313.     S_ENTER_NEW_KEY = 'Do you want to enter a new one?';
  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
  324.         if MessageDlg(Trim(msg + ' ' + S_ENTER_NEW_KEY), mtError, mbYesNoCancel, 0) = ID_YES then
  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;
  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.';
  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
  376.       raise EVtsCurConvException.CreateFmt(S_CANNOT_CREATE_DIR, [cacheDirName]);
  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
  400.         raise EVtsCurConvException.Create(S_INVALID_MAXAGE);
  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
  436.         _HandleKeyInvalidOrMissing(cacheFileName, S_NO_API_KEY_PROVIDED, doRetry, sJSON);
  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?'}
  448.         if ConfirmWebAccess and (MessageDlg(Format(S_DOWNLOAD_QUERY, [url, cacheFileName]), mtConfirmation, mbYesNoCancel, 0) <> ID_YES) then
  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;
  464.         if not assigned(xRoot) then raise EVtsCurConvException.Create(S_JSON_FILE_INVALID);
  465.  
  466.         xSuccess := xRoot.Field['success'] as TlkJSONboolean;
  467.         if not assigned(xSuccess) then raise EVtsCurConvException.Create(S_UNKNOWN_SUCCESS);
  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;
  488.             msg := S_JSON_UNKNOWN_ERROR;
  489.           end;
  490.           {$ENDREGION}
  491.  
  492.           if keyInvalid then
  493.           begin
  494.             _HandleKeyInvalidOrMissing(cacheFileName, S_API_KEY_INVALID, doRetry, sJSON);
  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.
  520.