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