unitStorageXML.pas 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. unit unitStorageXML;
  2. {$mode objfpc}{$H+}
  3. interface
  4. uses
  5. Classes, SysUtils, DOM, XMLRead, XMLWrite, Controls, Book, BookCollection,
  6. LazUTF8, LazFileUtils;
  7. { Load books from an XML file into AList. Clears the collection first.
  8. Parent is where TBook cover controls should be parented (e.g., PanelBackground). }
  9. procedure LoadBooksXML(const FileName: String; aList: TBookCollection; Parent: TWinControl);
  10. { Save AList to XML. Writes to <FileName>.tmp then atomically renames to FileName.
  11. Ensures no duplicate entries are written (based on a stable key). }
  12. procedure SaveBooksXML(const FileName: String; aList: TBookCollection);
  13. implementation
  14. function NormLower(const S: String): String; inline;
  15. begin
  16. Result := UTF8LowerCase(Trim(S));
  17. end;
  18. function KeyFor(const Title, Authors, Isbn, FilePath: String): String;
  19. var fp: String;
  20. begin
  21. if Trim(FilePath) <> '' then
  22. begin
  23. fp := ExpandFileName(FilePath);
  24. Exit('fp:' + NormLower(fp));
  25. end;
  26. if Trim(Isbn) <> '' then
  27. Exit('isbn:' + NormLower(Isbn));
  28. Exit('ti:' + NormLower(Title) + '|' + NormLower(Authors));
  29. end;
  30. procedure LoadBooksXML(const FileName: String; aList: TBookCollection; Parent: TWinControl);
  31. var
  32. Doc : TXMLDocument;
  33. Root : TDOMElement;
  34. Node : TDOMNode;
  35. El : TDOMElement;
  36. B : TBook;
  37. title, authors, isbn, filep, imagep: String;
  38. seen : TStringList;
  39. key : String;
  40. begin
  41. if (aList = nil) or (Parent = nil) then Exit;
  42. if not FileExistsUTF8(FileName) then
  43. begin
  44. // Nothing to load; ensure list is empty
  45. if aList.Count > 0 then
  46. aList.Clear;
  47. Exit;
  48. end;
  49. ReadXMLFile(Doc, FileName);
  50. try
  51. Root := Doc.DocumentElement; // <bookshelf version="1">
  52. if (Root = nil) or (UTF8LowerCase(Root.TagName) <> 'bookshelf') then
  53. Exit;
  54. // Start fresh to avoid duplication-on-startup
  55. aList.Clear;
  56. seen := TStringList.Create;
  57. try
  58. seen.Sorted := True; seen.Duplicates := dupIgnore;
  59. Node := Root.FirstChild;
  60. while Node <> nil do
  61. begin
  62. if (Node.NodeType = ELEMENT_NODE) then
  63. begin
  64. El := TDOMElement(Node);
  65. if UTF8LowerCase(El.TagName) = 'book' then
  66. begin
  67. // DOM returns UnicodeString; convert explicitly to UTF-8 to avoid warnings
  68. title := UTF8Encode(El.GetAttribute('title'));
  69. authors := UTF8Encode(El.GetAttribute('authors'));
  70. isbn := UTF8Encode(El.GetAttribute('isbn'));
  71. filep := UTF8Encode(El.GetAttribute('file'));
  72. imagep := UTF8Encode(El.GetAttribute('image'));
  73. key := KeyFor(title, authors, isbn, filep);
  74. if seen.IndexOf(key) < 0 then
  75. begin
  76. seen.Add(key);
  77. // Recreate the book object and add to collection
  78. B := TBook.Create(Parent);
  79. B.Title := title;
  80. B.Authors := authors;
  81. B.ISBN := isbn;
  82. if filep <> '' then B.FilePath := filep; // will try sibling images/pdf cover
  83. if imagep <> '' then B.ImagePath := imagep; // if a specific cover was saved
  84. // NOTE: If your BookCollection uses a different adder, adjust this line:
  85. aList.AddBook(B);
  86. end;
  87. end;
  88. end;
  89. Node := Node.NextSibling;
  90. end;
  91. finally
  92. seen.Free;
  93. end;
  94. finally
  95. Doc.Free;
  96. end;
  97. end;
  98. procedure SaveBooksXML(const FileName: String; aList: TBookCollection);
  99. var
  100. Doc : TXMLDocument;
  101. Root : TDOMElement;
  102. El : TDOMElement;
  103. i : Integer;
  104. B : TBook;
  105. tmp : String;
  106. seen : TStringList;
  107. key : String;
  108. begin
  109. if aList = nil then Exit;
  110. // Build XML document
  111. Doc := TXMLDocument.Create;
  112. try
  113. Root := Doc.CreateElement('bookshelf');
  114. Root.SetAttribute('version','1');
  115. Doc.AppendChild(Root);
  116. seen := TStringList.Create;
  117. try
  118. seen.Sorted := True; seen.Duplicates := dupIgnore;
  119. for i := 0 to aList.Count - 1 do
  120. begin
  121. B := aList.Books[i];
  122. key := KeyFor(B.Title, B.Authors, B.ISBN, B.FilePath);
  123. if seen.IndexOf(key) >= 0 then
  124. Continue; // skip duplicates in memory
  125. seen.Add(key);
  126. El := Doc.CreateElement('book');
  127. // Convert UTF-8 AnsiString to UnicodeString for XML writer
  128. El.SetAttribute('title', UTF8Decode(B.Title));
  129. El.SetAttribute('authors', UTF8Decode(B.Authors));
  130. El.SetAttribute('isbn', UTF8Decode(B.ISBN));
  131. El.SetAttribute('file', UTF8Decode(B.FilePath));
  132. El.SetAttribute('image', UTF8Decode(B.ImagePath));
  133. Root.AppendChild(El);
  134. end;
  135. finally
  136. seen.Free;
  137. end;
  138. // Atomic write: to .tmp then rename
  139. tmp := FileName + '.tmp';
  140. WriteXMLFile(Doc, tmp);
  141. // Replace using UTF-8 safe file ops
  142. if FileExistsUTF8(FileName) then
  143. DeleteFileUTF8(FileName);
  144. if not RenameFileUTF8(tmp, FileName) then
  145. raise Exception.CreateFmt('Failed to write %s', [FileName]);
  146. finally
  147. Doc.Free;
  148. end;
  149. end;
  150. end.