unit CS.Database.IO.Legacy;
interface
uses
// Delphi
SysUtils,
XMLIntf,
// 3rd party
Collections.Dictionaries,
// Project
CS.Database.Core.SnippetsTable,
CS.Database.Types,
CS.Utils.Dates,
UExceptions,
UXMLDocumentEx;
type
TDBLegacyUserDBReader = class(TObject)
strict private
const
// Database file name
DatabaseFileName = 'database.xml';
// watermark (never changes for all versions)
Watermark = '531257EA-1EE3-4B0F-8E46-C6E7F7140106';
// supported file format versions
EarliestVersion = 1;
LatestVersion = 6;
strict private
var
fDBPath: string;
fXMLDoc: IXMLDocumentEx; // Extended XML document object
fVersion: Integer;
fCategoryMap: TDictionary<string,string>;
procedure HandleException(E: Exception);
function LegacySnippetName(SnippetNode: IXMLNode): string;
procedure OpenXMLDoc;
procedure ReadCategoryInfo;
procedure ReadSnippets(const ATable: TDBSnippetsTable);
procedure LoadSnippet(SnippetNode: IXMLNode;
const ATable: TDBSnippetsTable);
procedure LoadSnippetProperties(SnippetNode: IXMLNode;
const ASnippet: TDBSnippet);
function GetLastModifiedFileDate(ATable: TDBSnippetsTable): TUTCDateTime;
public
constructor Create(const DBPath: string);
destructor Destroy; override;
procedure Load(const ATable: TDBSnippetsTable;
out ALastModified: TUTCDateTime);
end;
EDBLegacyUserDBReader = class(ECodeSnip);
implementation
uses
// Delphi
Character,
Classes,
IOUtils,
ActiveX,
XMLDom,
// 3rd party
Collections.Base,
// Project
CS.Database.SnippetLinks,
CS.Database.Snippets,
CS.Database.Tags,
CS.Markup,
CS.SourceCode.Languages,
ActiveText.UMain,
Compilers.UGlobals,
DB.USnippetKind,
UComparers,
UConsts,
UDOSDateTime,
UIOUtils,
UIStringList,
USnippetExtraHelper,
UStructs,
UStrUtils,
UXMLDocConsts,
UXMLDocHelper;
{ TDBLegacyUserDBReader }
resourcestring
// Error message
sMissingNode = 'Document has no %s node.';
sMissingSource = 'Source code file name missing for legacy snippet named '
+ '"%s"';
sFileNotFound = 'Database file "%s" missing.';
constructor TDBLegacyUserDBReader.Create(const DBPath: string);
var
StrRules: TRules<string>;
begin
inherited Create;
fDBPath := DBPath;
StrRules := TRules<string>.Create(
TTextComparer.Create, TTextEqualityComparer.Create
);
fCategoryMap := TDictionary<string,string>.Create(
StrRules, StrRules
);
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;
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 EDBSnippetID) or (E is EDBTag) 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.LegacySnippetName(SnippetNode: IXMLNode):
string;
begin
Result := SnippetNode.Attributes[cSnippetNameAttr];
end;
procedure TDBLegacyUserDBReader.Load(const ATable: TDBSnippetsTable;
out ALastModified: TUTCDateTime);
begin
ATable.Clear;
try
OpenXMLDoc;
ReadCategoryInfo;
ReadSnippets(ATable);
ALastModified := GetLastModifiedFileDate(ATable);
except
ATable.Clear;
if ExceptObject is Exception then
HandleException(ExceptObject as Exception);
end;
end;
procedure TDBLegacyUserDBReader.LoadSnippet(SnippetNode: IXMLNode;
const ATable: TDBSnippetsTable);
var
Snippet: TDBSnippet;
begin
Snippet := TDBSnippet.Create(
TDBSnippetID.Create(LegacySnippetName(SnippetNode))
);
try
LoadSnippetProperties(SnippetNode, Snippet);
ATable.Add(Snippet);
except
Snippet.Free;
raise;
end;
end;
procedure TDBLegacyUserDBReader.LoadSnippetProperties(SnippetNode: IXMLNode;
const ASnippet: TDBSnippet);
{TODO: Remove following routine when routine of same name becomes available in
UStrUtils. }
function StrIsBlank(const S: string): Boolean;
var
Ch: Char;
begin
if S = EmptyStr then
Exit(True);
for Ch in S do
if not TCharacter.IsWhiteSpace(Ch) then
Exit(False);
Result := True;
end;
// 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: IDBTagList;
var
CatID: string;
CatDesc: string;
begin
Result := TDBTagList.Create;
CatID := GetPropertyText(cCatIDNode);
if not fCategoryMap.TryGetValue(CatID, CatDesc) then
Exit;
Result.Add(TDBTag.Create(CatDesc));
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 GetKindProperty: TDBSnippetKind;
{Gets value of Kind node.
@return Kind that matches node value.
}
var
Default: TSnippetKind; // default value
LegacySnippetKind: TSnippetKind; //
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;
{ TODO: change TXMLDocHelper.GetSnippetKind to return TDBSnippetKind to get
rid of this convoluted conversion to TDBSnippetKind. }
LegacySnippetKind := TXMLDocHelper.GetSnippetKind(
fXMLDoc, SnippetNode, Default
);
case LegacySnippetKind of
TSnippetKind.skRoutine: Result := TDBSnippetKind.skRoutine;
TSnippetKind.skConstant: Result := TDBSnippetKind.skConstant;
TSnippetKind.skTypeDef: Result := TDBSnippetKind.skTypeDef;
TSnippetKind.skUnit: Result := TDBSnippetKind.skUnit;
TSnippetKind.skClass: Result := TDBSnippetKind.skClass;
else
Result := TDBSnippetKind.skFreeform;
end;
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: TMarkup;
var
ActiveText: IActiveText;
REML: string;
begin
// We get extra data from different nodes depending on file version
try
if fVersion = 1 then
begin
// version 1: build Notes from comments, credits and credits URL nodes.
ActiveText := TSnippetExtraHelper.BuildActiveText(
GetPropertyText(cCommentsNode),
GetPropertyText(cCreditsNode),
GetPropertyText(cCreditsUrlNode)
);
end
else
begin
// version 2 and later: build Notes from Extra node
REML := GetPropertyText(cExtraNode);
if StrIsBlank(REML) then
ActiveText := TActiveTextFactory.CreateActiveText
else
// Following conversion works with all versions of REML from v1 to v3
ActiveText := TSnippetExtraHelper.BuildActiveText(REML);
end;
if ActiveText.IsEmpty then
Result := TMarkup.CreateEmpty
else if ActiveText.IsPlainText then
Result := TMarkup.Create(ActiveText.ToString, mkPlainText)
else
// Active text is converted to REML v3 regardless of REML version used
// in database file.
Result := TMarkup.Create(
TSnippetExtraHelper.BuildREMLMarkup(ActiveText), mkREML3
);
except
// error: provide an empty property value
Result := TMarkup.CreateEmpty;
end;
end;
// Returns markup of snippets Description property. This is derived from the
// similar property of the legacy snippet.
function GetDescriptionProperty: TMarkup;
var
Desc: string; // text read from description node
begin
Desc := GetPropertyText(cDescriptionNode);
if not StrIsBlank(Desc) then
begin
if fVersion < 6 then
// versions before 6: description is stored as plain text
Result := TMarkup.Create(Desc, mkPlainText)
else
// version 6 & later: description is stored as REML v3
Result := TMarkup.Create(Desc, mkREML3);
end
else
Result := TMarkup.CreateEmpty;
end;
// Returns the text of the snippet's Title property. This is derived from the
// the legacy snippet's DisplayName or, if that is not provided, its Name
// property.
function GetTitleProperty: string;
begin
Result := GetPropertyText(cDisplayNameNode);
if StrIsBlank(Result) then
Result := LegacySnippetName(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.Create('Pascal')
else
Result := TSourceCodeLanguageID.Create('Text');
end;
// Converts the given list of legacy snippet names into a list of snippets
// with IDs based on the legacy names.
function ConvertSnippetIDList(LegacyNameList: IStringList): IDBSnippetIDList;
var
LegacyName: string;
begin
Result := TDBSnippetIDList.Create;
for LegacyName in LegacyNameList do
Result.Add(TDBSnippetID.Create(LegacyName));
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: TDBCompileResults;
var
LegacyCompileResults: TCompileResults;
CompilerID: TCompilerID;
Succeeds, Fails: TCompilerIDs;
begin
LegacyCompileResults := TXMLDocHelper.GetCompilerResults(
fXMLDoc, SnippetNode
);
Succeeds := [];
Fails := [];
for CompilerID := Low(TCompilerID) to High(TCompilerID) do
begin
if LegacyCompileResults[CompilerID] in [crSuccess, crWarning] then
Include(Succeeds, CompilerID)
else if LegacyCompileResults[CompilerID] = crError then
Include(Fails, CompilerID);
end;
Result := TDBCompileResults.Create(Succeeds, Fails);
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.SetKind(GetKindProperty);
ASnippet.SetCompileResults(GetCompileResultsProperty);
ASnippet.SetTags(GetTagsProperty);
// We create a link back to the snippet's original ID in case it might be
// useful. We use the special "LegacyDB" synch space.
ASnippet.SetLinkInfo(
TSnippetLinkInfo.Create(
TSnippetSynchSpaceIDs.LegacyDB,
TDBSnippetID.Create(LegacySnippetName(SnippetNode))
)
);
// Note that the snippet's TestInfo and Starred properties have no equivalent
// property in legacy snippets, so the values are not set here: defaults are
// acceptable.
end;
procedure TDBLegacyUserDBReader.OpenXMLDoc;
var
XMLFile: string;
begin
XMLFile := IncludeTrailingPathDelimiter(fDBPath) + DatabaseFileName;
if not TFile.Exists(XMLFile) then
raise EDBLegacyUserDBReader.CreateFmt(sFileNotFound, [DatabaseFileName]);
fXMLDoc.LoadFromFile(XMLFile);
fXMLDoc.Active := True;
TXMLDocHelper.ValidateProcessingInstr(fXMLDoc);
fVersion := TXMLDocHelper.ValidateRootNode(
fXMLDoc,
cUserDataRootNode,
Watermark,
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;
CatDesc: 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>
CatDesc := TXMLDocHelper.GetSubTagText(
fXMLDoc, CatNode, cDescriptionNode
);
fCategoryMap.Add(CatID, CatDesc);
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;
end.