Menu

[r4761]: / branches / parsnip / Src / Main / CS.Database.IO.Legacy.pas  Maximize  Restore  History

Download this file

609 lines (563 with data), 19.4 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
{
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/
*
* Copyright (C) 2013-2014, Peter Johnson (www.delphidabbler.com).
*
* $Rev$
* $Date$
*
* Implements a class that can load the snippets database in the formats used
* for the user database in CodeSnip v2 to v4.
}
unit CS.Database.IO.Legacy;
interface
uses
// Delphi
SysUtils,
XMLIntf,
// 3rd party
Collections.Dictionaries,
// Project
CS.Database.IO.Types,
CS.Database.SnippetsTable,
CS.Database.Types,
CS.Utils.Dates,
UExceptions,
UXMLDocumentEx;
type
TDBLegacyUserDBReader = class(TInterfacedObject, IDatabaseLoader)
strict private
const
// Database file name
DatabaseFileName = 'database.xml';
// watermark (never changes for all versions)
DatabaseFileWatermark = '531257EA-1EE3-4B0F-8E46-C6E7F7140106';
FavouritesFileWatermark = #$25BA + ' CodeSnip Favourites v1 ' + #$25C4;
// supported file format versions
EarliestVersion = 1;
LatestVersion = 6;
strict private
var
fDBPath: string;
fXMLDoc: IXMLDocumentEx; // Extended XML document object
fVersion: Integer;
/// <summary>List of (user-defined) favourite snippets read from any
/// available Favourites file.</summary>
/// <remarks>Any snippets read from XML file whose ID is in this list
/// will have their Starred property set to True.</remarks>
fFavourites: ISnippetIDList;
/// <summary>Maps category IDs to tag name based on category name.
/// </summary>
fCategoryMap: TDictionary<string,string>;
procedure HandleException(E: Exception);
function XMLFileName: string;
function LegacySnippetID(SnippetNode: IXMLNode): string;
procedure OpenXMLDoc;
procedure ReadCategoryInfo;
/// <summary>Loads information about user's favourites and records each
/// user-defined favourite snippet's ID.</summary>
procedure LoadFavourites;
procedure ReadSnippets(const ATable: TDBSnippetsTable);
procedure LoadSnippet(SnippetNode: IXMLNode;
const ATable: TDBSnippetsTable);
procedure LoadSnippetProperties(SnippetNode: IXMLNode;
const ASnippet: TDBSnippet);
function GetLastModifiedFileDate(ATable: TDBSnippetsTable): TUTCDateTime;
function BuildTagSet: ITagSet;
public
constructor Create(const DBPath: string);
destructor Destroy; override;
procedure Load(const ATable: TDBSnippetsTable; out ATagSet: ITagSet;
out ALastModified: TUTCDateTime);
function DatabaseExists: Boolean;
end;
EDBLegacyUserDBReader = class(ECodeSnip);
implementation
uses
// Delphi
Character,
Classes,
IOUtils,
ActiveX,
XMLDom,
// 3rd party
Collections.Base,
// Project
CS.ActiveText,
CS.ActiveText.Helper,
CS.Database.SnippetOrigins,
CS.Database.Snippets,
CS.Database.Tags,
CS.SourceCode.Languages,
Compilers.UGlobals,
UAppInfo,
UComparers,
UConsts,
UDOSDateTime,
UIOUtils,
UIStringList,
UStructs,
UStrUtils,
UXMLDocConsts,
UXMLDocHelper;
{ TDBLegacyUserDBReader }
resourcestring
// Error message
sMissingNode = 'Document has no %s node.';
sMissingSource = 'Source code file name missing for legacy snippet "%s"';
sFileNotFound = 'Database file "%s" missing.';
function TDBLegacyUserDBReader.BuildTagSet: ITagSet;
var
TagName: string;
begin
Result := TTagSet.Create;
for TagName in fCategoryMap.Values do
Result.Add(TTag.Create(TagName));
end;
constructor TDBLegacyUserDBReader.Create(const DBPath: string);
var
StrRules: TRules<string>;
begin
inherited Create;
fDBPath := DBPath;
StrRules := TRulesFactory<string>.CreateFromComparator(
TTextComparator.Create
);
fCategoryMap := TDictionary<string,string>.Create(StrRules, StrRules);
fFavourites := TSnippetIDList.Create;
fVersion := -1; // dummy value changed by OpenXMLDoc when Load is called
// For some reason we must call OleInitialize here rather than in
// initialization section
OleInitialize(nil);
fXMLDoc := TXMLDocHelper.CreateXMLDoc;
end;
function TDBLegacyUserDBReader.DatabaseExists: Boolean;
const
ChunkSize = 512;
XMLProcInst = '<?xml version="1.0"';
Watermark = '<codesnip-data watermark="531257EA-1EE3-4B0F-8E46-C6E7F7140106"';
var
Chunk: string; // chunk of up to 512 ASCII chars read from file
begin
Result := False;
try
if not TFile.Exists(XMLFileName, False) then
Exit;
// Beginning of file can be treated as ASCII. There is never a BOM
Chunk := TEncoding.ASCII.GetString(
TFileIO.ReadBytes(XMLFileName, ChunkSize)
);
if not StrStartsStr(XMLProcInst, Chunk) then
Exit; // Not XML
if not StrContainsStr(Watermark, Chunk) then
Exit; // No valid open tag or watermark attribute
Result := True;
except
// swallow any exception (Result will be False)
end;
end;
destructor TDBLegacyUserDBReader.Destroy;
begin
fCategoryMap.Free;
inherited;
end;
function TDBLegacyUserDBReader.GetLastModifiedFileDate(
ATable: TDBSnippetsTable): TUTCDateTime;
var
Snippet: TDBSnippet;
begin
Result := TUTCDateTime.CreateNull;
for Snippet in ATable do
begin
if Result < Snippet.GetModified then
Result := Snippet.GetModified;
end;
if Result.IsNull then
Result := TUTCDateTime.Now;
end;
procedure TDBLegacyUserDBReader.HandleException(E: Exception);
resourcestring
sXMLError = 'Error reading database file XML:' + EOL2 + '%s';
sParseError = 'Error parsing database file:' + EOL2 + '%s';
sSourceFileError = 'Error reading source code file:' + EOL2 + '%s';
begin
if (E is EXMLDocError) or (E is ECodeSnipXML) or (E is EDOMParseError) then
raise EDBLegacyUserDBReader.CreateFmt(sXMLError, [E.Message]);
if (E is ESnippetID) or (E is ETag) then
raise EDBLegacyUserDBReader.CreateFmt(sParseError, [E.Message]);
if (E is EIOUtils) or (E is EStreamError) then
raise EDBLegacyUserDBReader.CreateFmt(sSourceFileError, [E.Message]);
raise E;
end;
function TDBLegacyUserDBReader.LegacySnippetID(SnippetNode: IXMLNode): string;
begin
Result := SnippetNode.Attributes[cSnippetIDAttr];
end;
procedure TDBLegacyUserDBReader.Load(const ATable: TDBSnippetsTable;
out ATagSet: ITagSet; out ALastModified: TUTCDateTime);
begin
ATable.Clear;
try
// NOTE: Favourites and Category info must be loaded before
LoadFavourites;
OpenXMLDoc;
ReadCategoryInfo;
ReadSnippets(ATable);
ATagSet := BuildTagSet;
ALastModified := GetLastModifiedFileDate(ATable);
except
ATable.Clear;
if ExceptObject is Exception then
HandleException(ExceptObject as Exception);
end;
end;
procedure TDBLegacyUserDBReader.LoadFavourites;
var
Lines: IStringList;
Line: string;
Fields: IStringList;
SnippetID: TSnippetID;
UserDefined: Boolean;
begin
// NOTE: If any error is encountered we simply abandon the method rather than
// reporting the error via an exception. This is because an exception would
// crash the database load process: it's better to simply loose the favourite
// information.
if not TFile.Exists(TAppInfo.FavouritesFileName, False) then
Exit;
try
Lines := TIStringList.Create(
TFileIO.ReadAllLines(TAppInfo.FavouritesFileName, TEncoding.UTF8, True)
);
except
Exit;
end;
Line := Lines[0];
if Line <> FavouritesFileWatermark then
Exit;
Lines.Delete(0);
for Line in Lines do
begin
if StrIsBlank(Line) then
Continue;
Fields := TIStringList.Create(Line, TAB, False, True);
if Fields.Count <> 3 then
Exit;
SnippetID := TSnippetID.Create(Fields[0]);
// We only record snippets that are user defined because all legacy XML
// databases contained only user-defined snippets.
UserDefined := StrSameText(Fields[1], 'True');
if UserDefined then
fFavourites.Add(SnippetID);
end;
end;
procedure TDBLegacyUserDBReader.LoadSnippet(SnippetNode: IXMLNode;
const ATable: TDBSnippetsTable);
var
Snippet: TDBSnippet;
begin
Snippet := TDBSnippet.Create(TSnippetID.Create(LegacySnippetID(SnippetNode)));
try
LoadSnippetProperties(SnippetNode, Snippet);
// Starred property has no equivalent property in legacy database. However
// the CodeSnip 4 concept of "favourite" snippets maps nicely onto the
// Starred property. Favourites were not in the legacy database but in a
// "Favourites" file. Therefore any snippet listed in any inherited
// "Favourites" file has its Starred property set to True.
if fFavourites.Contains(Snippet.GetID) then
Snippet.SetStarred(True);
ATable.Add(Snippet);
except
Snippet.Free;
raise;
end;
end;
procedure TDBLegacyUserDBReader.LoadSnippetProperties(SnippetNode: IXMLNode;
const ASnippet: TDBSnippet);
// Gets text of property with given sub-tag of current snippet node in XML
// document.
function GetPropertyText(const PropTagName: string): string;
begin
Result := TXMLDocHelper.GetSubTagText(fXMLDoc, SnippetNode, PropTagName);
end;
// Returns value of current snippet'ss Tags property from data in XML
// document. A single tag is returned which uses as its text the description
// of the legacy snippet's category.
function GetTagsProperty: ITagSet;
var
CatID: string;
TagName: string;
begin
Result := TTagSet.Create;
CatID := GetPropertyText(cCatIDNode);
if not fCategoryMap.TryGetValue(CatID, TagName) then
Exit;
Result.Add(TTag.Create(TagName));
end;
// Returns the name of the file containing the snippet's source code.
function GetSourceCodeFileName: string;
var
DataFileName: string; // base name of source code file
begin
DataFileName := GetPropertyText(cSourceCodeFileNode);
if DataFileName = '' then
raise EDBLegacyUserDBReader.CreateFmt(
sMissingSource, [ASnippet.GetID.ToString]
);
Result := IncludeTrailingPathDelimiter(fDBPath) + DataFileName;
end;
// Returns the date and time the snippet's source code file was last saved.
function GetLastUpdateDate: TUTCDateTime;
var
DOSDate: IDOSDateTime;
begin
DOSDate := TDOSDateTimeFactory.CreateFromFile(GetSourceCodeFileName);
if DOSDate.DateStamp = -1 then
Exit(TUTCDateTime.CreateNull);
Result := TUTCDateTime.CreateFromLocalDateTime(
FileDateToDateTime(DOSDate.DateStamp)
);
end;
// Returns the snippet's source code, read from the data file referenced in
// the XML document.
function GetSourceCodeProperty: string;
var
Encoding: TEncoding;
begin
// Before database v5, source code files used default ANSI encoding. From v5
// UTF-8 with no BOM was used.
if fVersion < 5 then
Encoding := TEncoding.Default
else
Encoding := TEncoding.UTF8;
Result := TFileIO.ReadAllText(GetSourceCodeFileName, Encoding, False);
end;
// Returns the value of the XML document's <standard-format> node. Returns
// False if there is no such node.
function GetStandardFormatValue: Boolean;
{Gets value of standard format node.
@return True if standard format, False if not.
}
begin
Result := TXMLDocHelper.GetStandardFormat(
fXMLDoc, SnippetNode, False
);
end;
// Returns the value of the snippet's Kind property. This is a direct match
// with legacy snippet's property of the same name.
function GetKindIDProperty: TSnippetKindID;
{Gets value of Kind node.
@return Kind that matches node value.
}
var
Default: TSnippetKindID; // default value
begin
// In earlier file format versions we have no Kind node, so we calculate
// kind from StandardFormat value. If Kind node is present StandardFormat is
// ignored.
if GetStandardFormatValue then
Default := skRoutine
else
Default := skFreeform;
Result := TXMLDocHelper.GetSnippetKindID(fXMLDoc, SnippetNode, Default);
end;
// Returns mark-up of snippet's Notes property. This value is derived from
// either the legacy snippet's Comments and Credits properties or from its
// Extra property.
function GetNotesProperty: IActiveText;
begin
// We get Notes data from different nodes depending on file version
try
if fVersion = 1 then
// version 1: build Notes from comments, credits and credits URL
// nodes
Result := TActiveTextHelper.ParseCommentsAndCredits(
GetPropertyText(cCommentsNode),
GetPropertyText(cCreditsNode),
GetPropertyText(cCreditsUrlNode)
)
else
// version 2 & later: build Notes from REML in "extra" node
Result := TActiveTextHelper.ParseREML(
GetPropertyText(cExtraNode)
);
except
// error: provide an empty property value
Result := TActiveTextFactory.CreateActiveText;
end;
end;
// Returns markup of snippets Description property. This is derived from the
// similar property of the legacy snippet.
function GetDescriptionProperty: IActiveText;
var
Desc: string; // text read from description node
begin
Desc := GetPropertyText(cDescriptionNode);
if Desc <> '' then
begin
if fVersion < 6 then
// versions before 6: description is stored as plain text
Result := TActiveTextHelper.ParsePlainText(Desc)
else
// version 6 & later: description is stored as REML
Result := TActiveTextHelper.ParseREML(Desc)
end
else
Result := TActiveTextFactory.CreateActiveText;
end;
// Returns the text of the snippet's Title property. This is derived from the
// the legacy snippet's display name or, if that is not provided, its ID
// string.
function GetTitleProperty: string;
begin
Result := GetPropertyText(cTitleNode);
if StrIsBlank(Result) then
Result := LegacySnippetID(SnippetNode);
end;
// Returns the snippet's LanguageID property value which is deduced to be
// either Pascal or Text depending on whether the legacy snippet was to be
// highlighted or not.
function GetLanguageIDProperty: TSourceCodeLanguageID;
begin
if TXMLDocHelper.GetHiliteSource(fXMLDoc, SnippetNode, True) then
Result := TSourceCodeLanguageID.CreatePascal
else
Result := TSourceCodeLanguageID.CreatePlainText;
end;
// Converts the given list of legacy snippet ID strings into a list of
// snippets with IDs based on the legacy IDs.
function ConvertSnippetIDList(LegacyIDList: IStringList): ISnippetIDList;
var
LegacyID: string;
begin
Result := TSnippetIDList.Create;
for LegacyID in LegacyIDList do
Result.Add(TSnippetID.Create(LegacyID));
end;
// Reads a list of Pascal names from the given sub-tag of the current
// snippet's node in the XML document.
function GetPascalNameListFor(const SubTag: string): IStringList;
begin
Result := TIStringList.Create;
TXMLDocHelper.GetPascalNameList(
fXMLDoc, fXMLDoc.FindFirstChildNode(SnippetNode, SubTag), Result
);
end;
// Returns the snippet's CompileResults property value. This is derived from
// the legacy snippet's own CompileResult property, which has a different
// format to that of the new snippet.
function GetCompileResultsProperty: TCompileResults;
begin
Result := TXMLDocHelper.GetCompilerResults(
fXMLDoc, SnippetNode
);
end;
// ---------------------------------------------------------------------------
begin
ASnippet.SetCreated(GetLastUpdateDate);
ASnippet.SetModified(ASnippet.GetCreated);
ASnippet.SetTitle(GetTitleProperty);
ASnippet.SetDescription(GetDescriptionProperty);
ASnippet.SetSourceCode(GetSourceCodeProperty);
ASnippet.SetLanguageID(GetLanguageIDProperty);
ASnippet.SetRequiredModules(GetPascalNameListFor(cUnitsNode));
ASnippet.SetRequiredSnippets(
ConvertSnippetIDList(GetPascalNameListFor(cDependsNode))
);
ASnippet.SetXRefs(
ConvertSnippetIDList(GetPascalNameListFor(cXRefNode))
);
ASnippet.SetNotes(GetNotesProperty);
ASnippet.SetKindID(GetKindIDProperty);
ASnippet.SetCompileResults(GetCompileResultsProperty);
ASnippet.SetTags(GetTagsProperty);
// Origin property has no equivalent in legacy database, but we create one
// to record value was read from there and use snippet's last modification
// date in record.
ASnippet.SetOrigin(
TRemoteSnippetOrigin.Create(
sosLegacy,
LegacySnippetID(SnippetNode),
ASnippet.GetModified
)
);
// Note that the snippet's TestInfo and Starred properties have no equivalent
// property in legacy snippets database so they are not set here.
end;
procedure TDBLegacyUserDBReader.OpenXMLDoc;
var
XMLFile: string;
begin
XMLFile := XMLFileName;
if not TFile.Exists(XMLFile, False) then
raise EDBLegacyUserDBReader.CreateFmt(sFileNotFound, [DatabaseFileName]);
fXMLDoc.LoadFromFile(XMLFile);
fXMLDoc.Active := True;
TXMLDocHelper.ValidateProcessingInstr(fXMLDoc);
fVersion := TXMLDocHelper.ValidateRootNode(
fXMLDoc,
cUserDataRootNode,
DatabaseFileWatermark,
TRange.Create(EarliestVersion, LatestVersion)
);
// Both a categories and a snippets node must exist
if fXMLDoc.FindNode(cUserDataRootNode + '\' + cCategoriesNode) = nil then
raise EDBLegacyUserDBReader.CreateFmt(sMissingNode, [cCategoriesNode]);
if fXMLDoc.FindNode(cUserDataRootNode + '\' + cSnippetsNode) = nil then
raise EDBLegacyUserDBReader.CreateFmt(sMissingNode, [cSnippetsNode]);
end;
procedure TDBLegacyUserDBReader.ReadCategoryInfo;
var
CatListNode: IXMLNode;
CatNodes: IXMLSimpleNodeList;
CatNode: IXMLNode;
CatID: string;
TagName: string;
begin
// Find <categories> node
CatListNode := fXMLDoc.FindNode(cUserDataRootNode + '\' + cCategoriesNode);
if not Assigned(CatListNode) then
raise EDBLegacyUserDBReader.CreateFmt(sMissingNode, [cCategoriesNode]);
// Find all <category> nodes in <categories>
CatNodes := fXMLDoc.FindChildNodes(CatListNode, cCategoryNode);
fCategoryMap.Clear;
for CatNode in CatNodes do
begin
// Category ID is attribute of <category> node
CatID := CatNode.Attributes[cCategoryIdAttr];
if not fCategoryMap.ContainsKey(CatID) then
begin
// Category description is in <description> sub-tag of <category>. We
// convert description to a valid tag name and add to category->tag name
// map
TagName := TTag.MakeValidTagString(
TXMLDocHelper.GetSubTagText(
fXMLDoc, CatNode, cDescriptionNode
)
);
fCategoryMap.Add(CatID, TagName);
end;
end;
end;
procedure TDBLegacyUserDBReader.ReadSnippets(const ATable: TDBSnippetsTable);
var
SnippetListNode: IXMLNode;
SnippetNodes: IXMLSimpleNodeList;
SnippetNode: IXMLNode;
begin
// Find <routines> SnippetNode
SnippetListNode := fXMLDoc.FindNode(cUserDataRootNode + '\' + cSnippetsNode);
if not Assigned(SnippetListNode) then
raise EDBLegacyUserDBReader.CreateFmt(sMissingNode, [cSnippetsNode]);
// Find all <routine> nodes in <categories>
SnippetNodes := fXMLDoc.FindChildNodes(SnippetListNode, cSnippetNode);
for SnippetNode in SnippetNodes do
LoadSnippet(SnippetNode, ATable);
end;
function TDBLegacyUserDBReader.XMLFileName: string;
begin
Result := IncludeTrailingPathDelimiter(fDBPath) + DatabaseFileName;
end;
end.
Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.