Rev 69 | Go to most recent revision | Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
68 | daniel-mar | 1 | unit UD2_Obj; |
2 | |||
3 | interface |
||
4 | |||
5 | {$IF CompilerVersion >= 25.0} |
||
6 | {$LEGACYIFEND ON} |
||
7 | {$IFEND} |
||
8 | |||
9 | uses |
||
10 | Windows, SysUtils, Classes, IniFiles, Contnrs, Dialogs; |
||
11 | |||
12 | const |
||
13 | cchBufferSize = 2048; |
||
14 | |||
15 | type |
||
16 | TUD2Plugin = class(TObject) |
||
17 | protected |
||
18 | FDetectedIdentifications: TObjectList{<TUD2IdentificationEntry>}; |
||
19 | public |
||
20 | PluginDLL: string; |
||
21 | PluginGUID: TGUID; |
||
22 | PluginName: WideString; |
||
23 | PluginVendor: WideString; |
||
24 | PluginVersion: WideString; |
||
25 | IdentificationMethodName: WideString; |
||
26 | function PluginGUIDString: string; |
||
27 | property DetectedIdentifications: TObjectList{<TUD2IdentificationEntry>} |
||
28 | read FDetectedIdentifications; |
||
29 | destructor Destroy; override; |
||
30 | constructor Create; |
||
31 | procedure AddIdentification(IdStr: WideString); |
||
32 | end; |
||
33 | |||
34 | TUD2IdentificationEntry = class(TObject) |
||
35 | private |
||
36 | FIdentificationString: WideString; |
||
37 | FPlugin: TUD2Plugin; |
||
38 | public |
||
39 | property IdentificationString: WideString read FIdentificationString; |
||
40 | property Plugin: TUD2Plugin read FPlugin; |
||
41 | function GetPrimaryIdName: WideString; |
||
42 | procedure GetIdNames(sl: TStrings); |
||
43 | constructor Create(AIdentificationString: WideString; APlugin: TUD2Plugin); |
||
44 | end; |
||
45 | |||
46 | TUD2 = class(TObject) |
||
47 | private |
||
48 | FGUIDLookup: TStrings; |
||
49 | protected |
||
50 | FLoadedPlugins: TObjectList{<TUD2Plugin>}; |
||
51 | FIniFile: TMemIniFile; |
||
52 | FErrors: TStrings; |
||
53 | FIniFileName: string; |
||
54 | procedure HandleDLL(dllFile: string); |
||
55 | public |
||
56 | property IniFileName: string read FIniFileName; |
||
57 | property Errors: TStrings read FErrors; |
||
58 | property LoadedPlugins: TObjectList{<TUD2Plugin>} read FLoadedPlugins; |
||
59 | property IniFile: TMemIniFile read FIniFile; |
||
60 | procedure GetCommandList(ShortTaskName: string; outSL: TStrings); |
||
61 | procedure HandlePluginDir(APluginDir: string); |
||
62 | procedure GetTaskListing(outSL: TStrings); |
||
63 | constructor Create(AIniFileName: string); |
||
64 | destructor Destroy; override; |
||
65 | function TaskExists(ShortTaskName: string): boolean; |
||
66 | function ReadMetatagString(ShortTaskName, MetatagName: string; |
||
67 | DefaultVal: string): string; |
||
68 | function ReadMetatagBool(ShortTaskName, MetatagName: string; |
||
69 | DefaultVal: string): boolean; |
||
70 | function GetTaskName(AShortTaskName: string): string; |
||
71 | end; |
||
72 | |||
73 | implementation |
||
74 | |||
75 | uses |
||
76 | UD2_PluginIntf, UD2_Utils; |
||
77 | |||
78 | function UD2_ErrorLookup(ec: UD2_STATUSCODE): string; |
||
79 | resourcestring |
||
80 | LNG_STATUS_OK = 'Operation completed sucessfully'; |
||
81 | LNG_STATUS_BUFFER_TOO_SMALL = 'The provided buffer is too small!'; |
||
82 | LNG_STATUS_INVALID_ARGS = 'The function received invalid arguments!'; |
||
83 | LNG_STATUS_INVALID = 'Unexpected status code %s'; |
||
84 | LNG_STATUS_NOT_LICENSED = 'The plugin is not licensed'; |
||
85 | begin |
||
86 | if ec = UD2_STATUS_OK then result := LNG_STATUS_OK |
||
87 | else if ec = UD2_STATUS_BUFFER_TOO_SMALL then result := LNG_STATUS_BUFFER_TOO_SMALL |
||
88 | else if ec = UD2_STATUS_INVALID_ARGS then result := LNG_STATUS_INVALID_ARGS |
||
89 | else if ec = UD2_STATUS_NOT_LICENSED then result := LNG_STATUS_NOT_LICENSED |
||
90 | else result := Format(LNG_STATUS_INVALID, ['0x'+IntToHex(ec, 8)]); |
||
91 | end; |
||
92 | |||
93 | { TUD2Plugin } |
||
94 | |||
95 | function TUD2Plugin.PluginGUIDString: string; |
||
96 | begin |
||
97 | result := UpperCase(GUIDToString(PluginGUID)); |
||
98 | end; |
||
99 | |||
100 | procedure TUD2Plugin.AddIdentification(IdStr: WideString); |
||
101 | begin |
||
102 | DetectedIdentifications.Add(TUD2IdentificationEntry.Create(IdStr, Self)) |
||
103 | end; |
||
104 | |||
105 | destructor TUD2Plugin.Destroy; |
||
106 | begin |
||
107 | DetectedIdentifications.Free; |
||
108 | inherited; |
||
109 | end; |
||
110 | |||
111 | constructor TUD2Plugin.Create; |
||
112 | begin |
||
113 | inherited Create; |
||
114 | FDetectedIdentifications := TObjectList{<TUD2IdentificationEntry>}.Create(true); |
||
115 | end; |
||
116 | |||
117 | { TUD2IdentificationEntry } |
||
118 | |||
119 | function TUD2IdentificationEntry.GetPrimaryIdName: WideString; |
||
120 | begin |
||
121 | result := Plugin.IdentificationMethodName+':'+IdentificationString; |
||
122 | end; |
||
123 | |||
124 | procedure TUD2IdentificationEntry.GetIdNames(sl: TStrings); |
||
125 | begin |
||
126 | sl.Add(GetPrimaryIdName); |
||
127 | sl.Add(UpperCase(Plugin.IdentificationMethodName)+':'+IdentificationString); |
||
128 | sl.Add(LowerCase(Plugin.IdentificationMethodName)+':'+IdentificationString); |
||
129 | sl.Add(UpperCase(Plugin.PluginGUIDString)+':'+IdentificationString); |
||
130 | sl.Add(LowerCase(Plugin.PluginGUIDString)+':'+IdentificationString); |
||
131 | end; |
||
132 | |||
133 | constructor TUD2IdentificationEntry.Create(AIdentificationString: WideString; |
||
134 | APlugin: TUD2Plugin); |
||
135 | begin |
||
136 | inherited Create; |
||
137 | FIdentificationString := AIdentificationString; |
||
138 | FPlugin := APlugin; |
||
139 | end; |
||
140 | |||
141 | { TUD2 } |
||
142 | |||
143 | procedure TUD2.HandleDLL(dllFile: string); |
||
144 | |||
145 | procedure ReportError(AMsg: string); |
||
146 | begin |
||
147 | // MessageDlg(AMsg, mtError, [mbOk], 0); |
||
148 | Errors.Add(AMsg) |
||
149 | end; |
||
150 | |||
151 | var |
||
152 | sIdentifier: array[0..cchBufferSize-1] of WideChar; |
||
153 | sIdentifiers: TArrayOfString; |
||
154 | sPluginName: array[0..cchBufferSize-1] of WideChar; |
||
155 | sPluginVendor: array[0..cchBufferSize-1] of WideChar; |
||
156 | sPluginVersion: array[0..cchBufferSize-1] of WideChar; |
||
157 | sIdentificationMethodName: array[0..cchBufferSize-1] of WideChar; |
||
158 | sPluginConfigFile: string; |
||
159 | iniConfig: TINIFile; |
||
160 | sOverrideGUID: string; |
||
161 | pluginID: TGUID; |
||
162 | sPluginID: string; |
||
163 | pluginInterfaceID: TGUID; |
||
164 | dllHandle: cardinal; |
||
165 | fPluginInterfaceID: TFuncPluginInterfaceID; |
||
166 | fPluginIdentifier: TFuncPluginIdentifier; |
||
167 | fPluginNameW: TFuncPluginNameW; |
||
168 | fPluginVendorW: TFuncPluginVendorW; |
||
169 | fPluginVersionW: TFuncPluginVersionW; |
||
170 | fIdentificationMethodNameW: TFuncIdentificationMethodNameW; |
||
171 | fIdentificationStringW: TFuncIdentificationStringW; |
||
172 | fCheckLicense: TFuncCheckLicense; |
||
173 | statusCode: UD2_STATUSCODE; |
||
174 | pl: TUD2Plugin; |
||
175 | i: integer; |
||
176 | lngID: LANGID; |
||
177 | resourcestring |
||
178 | LNG_DLL_NOT_LOADED = 'Plugin DLL "%s" could not be loaded.'; |
||
179 | LNG_METHOD_NOT_FOUND = 'Method "%s" not found in plugin "%s". The DLL is probably not a valid plugin DLL.'; |
||
180 | LNG_INVALID_PLUGIN = 'The plugin "%s" is not a valid plugin for this program version.'; |
||
181 | LNG_METHOD_FAILURE = 'Error "%s" at method "%s" of plugin "%s".'; |
||
182 | LNG_PLUGINS_SAME_GUID = 'Attention: The plugin "%s" and the plugin "%s" have the same identification GUID. The latter will not be loaded.'; |
||
183 | begin |
||
184 | lngID := GetSystemDefaultLangID; |
||
185 | |||
186 | dllHandle := LoadLibrary(PChar(dllFile)); |
||
187 | if dllHandle = 0 then |
||
188 | begin |
||
189 | ReportError(Format(LNG_DLL_NOT_LOADED, [dllFile])); |
||
190 | end; |
||
191 | try |
||
192 | @fPluginInterfaceID := GetProcAddress(dllHandle, mnPluginInterfaceID); |
||
193 | if not Assigned(fPluginInterfaceID) then |
||
194 | begin |
||
195 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnPluginInterfaceID, dllFile])); |
||
196 | Exit; |
||
197 | end; |
||
198 | pluginInterfaceID := fPluginInterfaceID(); |
||
199 | if not IsEqualGUID(pluginInterfaceID, GUID_USERDETECT2_IDPLUGIN_V1) then |
||
200 | begin |
||
201 | ReportError(Format(LNG_INVALID_PLUGIN, [dllFile])); |
||
202 | Exit; |
||
203 | end; |
||
204 | |||
205 | @fIdentificationStringW := GetProcAddress(dllHandle, mnIdentificationStringW); |
||
206 | if not Assigned(fIdentificationStringW) then |
||
207 | begin |
||
208 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnIdentificationStringW, dllFile])); |
||
209 | Exit; |
||
210 | end; |
||
211 | |||
212 | @fPluginNameW := GetProcAddress(dllHandle, mnPluginNameW); |
||
213 | if not Assigned(fPluginNameW) then |
||
214 | begin |
||
215 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnPluginNameW, dllFile])); |
||
216 | Exit; |
||
217 | end; |
||
218 | |||
219 | @fPluginVendorW := GetProcAddress(dllHandle, mnPluginVendorW); |
||
220 | if not Assigned(fPluginVendorW) then |
||
221 | begin |
||
222 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnPluginVendorW, dllFile])); |
||
223 | Exit; |
||
224 | end; |
||
225 | |||
226 | @fPluginVersionW := GetProcAddress(dllHandle, mnPluginVersionW); |
||
227 | if not Assigned(fPluginVersionW) then |
||
228 | begin |
||
229 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnPluginVersionW, dllFile])); |
||
230 | Exit; |
||
231 | end; |
||
232 | |||
233 | @fCheckLicense := GetProcAddress(dllHandle, mnCheckLicense); |
||
234 | if not Assigned(fCheckLicense) then |
||
235 | begin |
||
236 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnCheckLicense, dllFile])); |
||
237 | Exit; |
||
238 | end; |
||
239 | |||
240 | @fIdentificationMethodNameW := GetProcAddress(dllHandle, mnIdentificationMethodNameW); |
||
241 | if not Assigned(fIdentificationMethodNameW) then |
||
242 | begin |
||
243 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnIdentificationMethodNameW, dllFile])); |
||
244 | Exit; |
||
245 | end; |
||
246 | |||
247 | sPluginID := ''; |
||
248 | |||
249 | sPluginConfigFile := ChangeFileExt(dllFile, '.ini'); |
||
250 | if FileExists(sPluginConfigFile) then |
||
251 | begin |
||
252 | iniConfig := TIniFile.Create(sPluginConfigFile); |
||
253 | try |
||
254 | sOverrideGUID := iniConfig.ReadString('Compatibility', 'OverrideGUID', ''); |
||
255 | if sOverrideGUID <> '' then |
||
256 | begin |
||
257 | sPluginID := sOverrideGUID; |
||
258 | pluginID := StringToGUID(sPluginID); |
||
259 | end; |
||
260 | finally |
||
261 | iniConfig.Free; |
||
262 | end; |
||
263 | end; |
||
264 | |||
265 | if sPluginID = '' then |
||
266 | begin |
||
267 | @fPluginIdentifier := GetProcAddress(dllHandle, mnPluginIdentifier); |
||
268 | if not Assigned(fPluginIdentifier) then |
||
269 | begin |
||
270 | ReportError(Format(LNG_METHOD_NOT_FOUND, [mnPluginIdentifier, dllFile])); |
||
271 | Exit; |
||
272 | end; |
||
273 | pluginID := fPluginIdentifier(); |
||
274 | sPluginID := GUIDToString(pluginID); |
||
275 | end; |
||
276 | |||
277 | if (FGUIDLookup.Values[sPluginID] <> '') and (FGUIDLookup.Values[sPluginID] <> dllFile) then |
||
278 | begin |
||
279 | ReportError(Format(LNG_PLUGINS_SAME_GUID, [FGUIDLookup.Values[sPluginID], dllFile])); |
||
280 | Exit; |
||
281 | end |
||
282 | else |
||
283 | begin |
||
284 | FGUIDLookup.Values[GUIDToString(pluginID)] := dllFile; |
||
285 | end; |
||
286 | |||
287 | statusCode := fCheckLicense(nil); |
||
288 | if statusCode <> UD2_STATUS_OK then |
||
289 | begin |
||
290 | ReportError(Format(LNG_METHOD_FAILURE, [UD2_ErrorLookup(statusCode), mnCheckLicense, dllFile])); |
||
291 | Exit; |
||
292 | end; |
||
293 | |||
294 | statusCode := fPluginNameW(@sPluginName, cchBufferSize, lngID); |
||
295 | if statusCode <> UD2_STATUS_OK then |
||
296 | begin |
||
297 | ReportError(Format(LNG_METHOD_FAILURE, [UD2_ErrorLookup(statusCode), mnPluginNameW, dllFile])); |
||
298 | Exit; |
||
299 | end; |
||
300 | |||
301 | statusCode := fPluginVendorW(@sPluginVendor, cchBufferSize, lngID); |
||
302 | if statusCode <> UD2_STATUS_OK then |
||
303 | begin |
||
304 | ReportError(Format(LNG_METHOD_FAILURE, [UD2_ErrorLookup(statusCode), mnPluginVendorW, dllFile])); |
||
305 | Exit; |
||
306 | end; |
||
307 | |||
308 | statusCode := fPluginVersionW(@sPluginVersion, cchBufferSize, lngID); |
||
309 | if statusCode <> UD2_STATUS_OK then |
||
310 | begin |
||
311 | ReportError(Format(LNG_METHOD_FAILURE, [UD2_ErrorLookup(statusCode), mnPluginVersionW, dllFile])); |
||
312 | Exit; |
||
313 | end; |
||
314 | |||
315 | statusCode := fIdentificationMethodNameW(@sIdentificationMethodName, cchBufferSize); |
||
316 | if statusCode <> UD2_STATUS_OK then |
||
317 | begin |
||
318 | ReportError(Format(LNG_METHOD_FAILURE, [UD2_ErrorLookup(statusCode), mnIdentificationMethodNameW, dllFile])); |
||
319 | Exit; |
||
320 | end; |
||
321 | |||
322 | pl := TUD2Plugin.Create; |
||
323 | pl.PluginDLL := dllFile; |
||
324 | pl.PluginGUID := pluginID; |
||
325 | pl.PluginName := sPluginName; |
||
326 | pl.PluginVendor := sPluginVendor; |
||
327 | pl.PluginVersion := sPluginVersion; |
||
328 | pl.IdentificationMethodName := sIdentificationMethodName; |
||
329 | LoadedPlugins.Add(pl); |
||
330 | |||
331 | statusCode := fIdentificationStringW(@sIdentifier, cchBufferSize); |
||
332 | if statusCode <> UD2_STATUS_OK then |
||
333 | begin |
||
334 | ReportError(Format(LNG_METHOD_FAILURE, [UD2_ErrorLookup(statusCode), mnIdentificationStringW, dllFile])); |
||
335 | Exit; |
||
336 | end; |
||
337 | |||
338 | if sIdentifier = '' then Exit; |
||
339 | |||
340 | // Multiple identifiers (e.g. multiple MAC addresses are delimited via #10 ) |
||
341 | SetLength(sIdentifiers, 0); |
||
342 | sIdentifiers := SplitString(UD2_MULTIPLE_ITEMS_DELIMITER, sIdentifier); |
||
343 | for i := Low(sIdentifiers) to High(sIdentifiers) do |
||
344 | begin |
||
345 | pl.AddIdentification(sIdentifiers[i]); |
||
346 | end; |
||
347 | finally |
||
348 | FreeLibrary(dllHandle); |
||
349 | end; |
||
350 | end; |
||
351 | |||
352 | procedure TUD2.HandlePluginDir(APluginDir: string); |
||
353 | Var |
||
354 | SR: TSearchRec; |
||
355 | path: string; |
||
356 | begin |
||
357 | path := IncludeTrailingPathDelimiter(APluginDir); |
||
358 | if FindFirst(path + '*.dll', 0, SR) = 0 then |
||
359 | begin |
||
360 | repeat |
||
361 | try |
||
362 | HandleDLL(path + sr.Name); |
||
363 | except |
||
364 | on E: Exception do |
||
365 | begin |
||
366 | MessageDlg(E.Message, mtError, [mbOK], 0); |
||
367 | end; |
||
368 | end; |
||
369 | until FindNext(SR) <> 0; |
||
370 | FindClose(SR); |
||
371 | end; |
||
372 | end; |
||
373 | |||
374 | destructor TUD2.Destroy; |
||
375 | begin |
||
376 | FIniFile.Free; |
||
377 | FLoadedPlugins.Free; |
||
378 | FGUIDLookup.Free; |
||
379 | FErrors.Free; |
||
380 | end; |
||
381 | |||
382 | constructor TUD2.Create(AIniFileName: string); |
||
383 | begin |
||
384 | FIniFileName := AIniFileName; |
||
385 | FLoadedPlugins := TObjectList{<TUD2Plugin>}.Create(true); |
||
386 | FIniFile := TMemIniFile.Create(IniFileName); |
||
387 | FGUIDLookup := TStringList.Create; |
||
388 | FErrors := TStringList.Create; |
||
389 | end; |
||
390 | |||
391 | function TUD2.GetTaskName(AShortTaskName: string): string; |
||
392 | begin |
||
393 | result := FIniFile.ReadString(AShortTaskName, 'Description', '('+AShortTaskName+')'); |
||
394 | end; |
||
395 | |||
396 | procedure TUD2.GetTaskListing(outSL: TStrings); |
||
397 | var |
||
398 | sl: TStringList; |
||
399 | i: integer; |
||
400 | desc: string; |
||
401 | begin |
||
402 | sl := TStringList.Create; |
||
403 | try |
||
404 | FIniFile.ReadSections(sl); |
||
405 | for i := 0 to sl.Count-1 do |
||
406 | begin |
||
407 | desc := GetTaskName(sl.Strings[i]); |
||
408 | outSL.Values[sl.Strings[i]] := desc; |
||
409 | end; |
||
410 | finally |
||
411 | sl.Free; |
||
412 | end; |
||
413 | end; |
||
414 | |||
415 | function TUD2.TaskExists(ShortTaskName: string): boolean; |
||
416 | begin |
||
417 | result := FIniFile.SectionExists(ShortTaskName); |
||
418 | end; |
||
419 | |||
420 | function TUD2.ReadMetatagString(ShortTaskName, MetatagName: string; |
||
421 | DefaultVal: string): string; |
||
422 | begin |
||
423 | result := IniFile.ReadString(ShortTaskName, MetatagName, DefaultVal); |
||
424 | end; |
||
425 | |||
426 | function TUD2.ReadMetatagBool(ShortTaskName, MetatagName: string; |
||
427 | DefaultVal: string): boolean; |
||
428 | begin |
||
429 | // DefaultVal is a string, because we want to allow an empty string, in case the |
||
430 | // user wishes an Exception in case the string is not a valid boolean string |
||
431 | result := BetterInterpreteBool(IniFile.ReadString(ShortTaskName, MetatagName, DefaultVal)); |
||
432 | end; |
||
433 | |||
434 | (* |
||
435 | |||
436 | NAMING EXAMPLE: ComputerName:ABC&&User:John=calc.exe |
||
437 | |||
438 | idTerm: ComputerName:ABC&&User:John |
||
439 | idName: ComputerName:ABC |
||
440 | IdMethodName: ComputerName |
||
441 | IdStr ABC |
||
442 | cmd: calc.exe |
||
443 | |||
444 | *) |
||
445 | |||
446 | procedure TUD2.GetCommandList(ShortTaskName: string; outSL: TStrings); |
||
447 | var |
||
448 | i, j: integer; |
||
449 | cmd: string; |
||
450 | idTerm, idName: WideString; |
||
451 | slSV, slIdNames: TStrings; |
||
452 | x: TArrayOfString; |
||
453 | nameVal: TArrayOfString; |
||
454 | FulfilsEverySubterm: boolean; |
||
455 | pl: TUD2Plugin; |
||
456 | ude: TUD2IdentificationEntry; |
||
457 | begin |
||
458 | SetLength(x, 0); |
||
459 | SetLength(nameVal, 0); |
||
460 | |||
461 | slIdNames := TStringList.Create; |
||
462 | try |
||
463 | for i := 0 to LoadedPlugins.Count-1 do |
||
464 | begin |
||
465 | pl := LoadedPlugins.Items[i] as TUD2Plugin; |
||
466 | for j := 0 to pl.DetectedIdentifications.Count-1 do |
||
467 | begin |
||
468 | ude := pl.DetectedIdentifications.Items[j] as TUD2IdentificationEntry; |
||
469 | ude.GetIdNames(slIdNames); |
||
470 | end; |
||
471 | end; |
||
472 | |||
473 | slSV := TStringList.Create; |
||
474 | try |
||
475 | FIniFile.ReadSectionValues(ShortTaskName, slSV); |
||
476 | for j := 0 to slSV.Count-1 do |
||
477 | begin |
||
478 | // We are doing the interpretation of the line ourselves, because |
||
479 | // TStringList.Values[] would not allow multiple command lines with the |
||
480 | // same key (idTerm) |
||
481 | nameVal := SplitString('=', slSV.Strings[j]); |
||
482 | idTerm := nameVal[0]; |
||
483 | cmd := nameVal[1]; |
||
484 | |||
485 | if Pos(':', idTerm) = 0 then Continue; |
||
486 | x := SplitString('&&', idTerm); |
||
487 | FulfilsEverySubterm := true; |
||
488 | for i := Low(x) to High(x) do |
||
489 | begin |
||
490 | idName := x[i]; |
||
491 | |||
492 | if slIdNames.IndexOf(idName) = -1 then |
||
493 | begin |
||
494 | FulfilsEverySubterm := false; |
||
495 | break; |
||
496 | end; |
||
497 | end; |
||
498 | |||
499 | if FulfilsEverySubterm then outSL.Add(cmd); |
||
500 | end; |
||
501 | finally |
||
502 | slSV.Free; |
||
503 | end; |
||
504 | finally |
||
505 | slIdNames.Free; |
||
506 | end; |
||
507 | end; |
||
508 | |||
509 | end. |