As you probably know by now (or not) SimpleStorage is a set of interfaces to ease the manipulation of XML documents. It is based on OmniXML, a wonderful Delphi XML parser. Simple Storage aims to treat XML as a practical data storage and means of data serialization and transportation. I have written about its usage in a series of articles, some time ago. Here are the articles:

From then, the framework has matured and I use it in a lot of my software. It has proven invaluable in terms of time savings and ease of XML manipulation. After all I have written it because I was tired of doing the same XML tasks over and over again. Recently, I upgraded it with two new important units . They cover two aspects of XML manipulation:

  • Encryption on a document scale
  • Working with collection of documents

I also did an upgrade of the core IElement interface. The goal was easier node attributes manipulation. I will show examples of all three improvements here. The new version of SimpleStorage can be found on my download page as always.

Encryption on a document scale

I recently had the need to encrypt whole XML documents. Since I already have SimpleStorage and XTEA encryption algorithm in my library, this was a no brainer. Whole documents can now be encrypted with some very simple code:

  EncryptedStorage('MyVeryStrongKey').SaveToFile(StorageFromFile(OpenDialog.FileName), SaveDialog.FileName);

And decrypted just as easily.

  EncryptedStorage('MyVeryStrongKey').LoadFromFile(OpenDialog.FileName).SaveToFile(SaveDialog.FileName);

I have intentionally wrote this in one line, because I always want to show how compact and powerfull the whole framework is. It tends to minimize tedious code and make your life easier. In real apps this should probably be two lines for easy redability.

  Storage := StorageFromFile(OpenDialog.FileName);
  EncryptedStorage('MyVeryStrongKey').SaveToFile(Storage , SaveDialog.FileName);

You can also encrypt / decrypt directly but you can look for that yourself. The code is obvious.

Easier node attributes manipulation

As I said, I strive to make the code compact and clear and attribute manipulation was one thing, that bugged me from the beginning of SimpleStorage. Everything else was very compact, but if you wanted to manipulate attribute “Language” of node “Book”, for instance, you had to write something like the following code. The first line is setting “Language” attribute of the “Book” node, by first getting the “Book” node and the second is setting the “Language” directly from “Book” node.

  CurrentNode.Get('Book').Attributes.Ensure('Language').AsString := 'English';
  CurrentNode.Attributes.Ensure('Language').AsString := 'English';

Which is ok, but it stand out from other SimpleStorage code. So I added “shortcuts” to the IElement interface. Now you can do the same like this:

  CurrentNode.EnsureAttr('Book', 'Language').AsString := 'English';
  CurrentNode.EnsureAttr('Language').AsString := 'English';

It saves a lot of characters on the long run :)

Working with collection of documents

Until now, SimpleStorage was great if you wanted to manipulate a single XML document. But what if you have lots of those and you wanted to process all of them. You had to write your own code (which is easy of course, but again repetitive work) and work with each document separately. Well no more. You now have “IDocuments interface that enables you to work over collections of XML documents. I will show this on a simple example. This was a real task, I had in front of me a few days ago. I had a bunch of XML documents looking like this:

<Devices>
  <Device ID="002694350026014400780050BF3F5173" InBound="23632" OutBound="13092" />
  <Device ID="002694350026012900880050BF3F5173" InBound="14728" OutBound="41084" />
  <Device ID="002694359925017200280050BF3F5173" InBound="22116" OutBound="61808" />
  <Device ID="002694350026013800980050BF3F5173" InBound="15140" OutBound="54612" />
  <Device ID="002694359725014400580050BF3F5173" InBound="13460" OutBound="34064" />
  <Device ID="002694359925013100880050BF3F5173" InBound="16332" OutBound="47120" />
  <Device ID="002694359625013600380050BF3F5173" InBound="14860" OutBound="47752" />
  <Device ID="002694359625014900680050BF3F5173" InBound="6120" OutBound="10852" />
  <Device ID="002694359925016600480050BF3F5173" InBound="11724" OutBound="46016" />
  <Device ID="002694350026011900980050BF3F5173" InBound="6404" OutBound="13540" />
  <Device ID="002694359925013600780050BF3F5173" InBound="7616" OutBound="10376" />
  <Device ID="002694359525017200080050BF3F5173" InBound="3060" OutBound="9996" />
  <Device ID="002694359925012400380050BF3F5173" InBound="460" OutBound="34084" />
  <Device ID="002694359725010700280050BF3F5173" InBound="8228" OutBound="35664" />
  <Device ID="002694350026012000780050BF3F5173" InBound="4076" OutBound="26664" />
</Devices>

This is a shortened version, but you get the idea. It is an XML that holds the “InBound” and “OutBound” traffic for mobile devices, over the HTTP protocol. In short, it enables me to see how much traffic my application generates for a single device. The shown XML, is a log for a single month of traffic. And in over two years, quite a few of these files accumulated. It would be easy to make a sum of  “InBound” and “OutBound” traffic and  report that for a single device over the complete time span in a traditional way. But lets see how we can do that with SimpleStorage. First let me show how to get all log entries for a single device. All XML logs were stored in “s:\bandwidth” directory.

procedure TForm1.Button2Click(Sender: TObject);
var
  Documents: IDocuments;
begin
  Documents := CreateCollection('s:\bandwidth', False);
  Documents.Get('//Device[@ID="3FBF5000735108010E1212674B5D036A"]',
    procedure(const Document: string; const Element: IElement; const Data: Pointer)
    begin
      ListBox1.Items.Add(Document + ' - ' + Element.Attributes.Get('InBound').AsString);
    end
  );
end;

Thats it. Simple isn’t it. Try to do the same with classic MS XML and compare the result.  Let me tell you again what I just did in these few lines of code. First I acuired all XML documents in a sinlge directory. Then I enumerated all the documents, but only processed the ones that have a distinct node (based on XPath) in them. Finally I printed some results for the selected nodes. If you dislike the anonymous approach you can also do it with classic callback functions. The interface supports both. the Get function / procedure is overloaded and it returns documents collection or it enumerates nodes over all documents based on XPath.

If you want to filter the collection of documents you have at your disposal you can do it like I have shown in this next example. This is just an example to show you how, it has no real value. In the callback you decide if the document will be included in the result collection or not. Then the code enumerates all documents and prints out a single attribute.

procedure TForm1.Button3Click(Sender: TObject);
var
  Documents: IDocuments;
  Document: ISimpleStorage;
  Element: IElement;
  Subset: IDocuments;
begin
  ListBox1.Clear;
  Randomize;
 
  Documents := CreateCollection('s:\bandwidth', False);
  Subset := Documents.Get('//Device[@ID="3FBF5000735108010E1212674B5D036A"]',
    procedure(const Document: ISimpleStorage; const FileName: string; var Include: Boolean; const Data: Pointer)
    begin
      Include := Random(10) > 5;
    end
  );
 
  for Document in Subset do
  begin
    Element := Document.Get('//Device[@ID="3FBF5000735108010E1212674B5D036A"]');
    ListBox1.Items.Add(Element.GetAttr('InBound').AsString);
  end;
end;

Now finally let me show the real code that helped me sumarize the traffic report. I used a classic callback here for sake of redability.

type
  TDeviceObject = class
    InBound: Int64;
    OutBound: Int64;
  end;
 
procedure TForm1.OnDeviceCallback(const Document: string; const Element: IElement; const Data: Pointer);
var
  DeviceIdx: Integer;
  HashTable: TStringList;
  DeviceObject: TDeviceObject;
begin
  HashTable := TStringList(Data);
  DeviceIdx := HashTable.IndexOf(Element.GetAttr('ID').AsString);
 
  if DeviceIdx = -1 then
    DeviceIdx := HashTable.AddObject(Element.GetAttr('ID').AsString, TDeviceObject.Create);
 
  DeviceObject := TDeviceObject(HashTable.Objects[DeviceIdx]);
  DeviceObject.InBound := DeviceObject.InBound + Element.GetAttr('InBound').AsInteger;
  DeviceObject.OutBound := DeviceObject.OutBound + Element.GetAttr('OutBound').AsInteger;
end;
 
procedure TForm1.Button1Click(Sender: TObject);
var
  I: Integer;
  HashTable: TStringList;
  Documents: IDocuments;
  OutputData: TStringList;
  DeviceObject: TDeviceObject;
begin
  HashTable := TStringList.Create;
  try
    Documents := CreateCollection('s:\bandwidth', False);
    Documents.Get('//Device', OnDeviceCallback, HashTable);
 
    OutputData := TStringList.Create;
    try
      for I := 0 to HashTable.Count - 1 do
      begin
       DeviceObject := TDeviceObject(HashTable.Objects[I]);
       OutputData.Add(Format('%s,%d,%d', [HashTable[I], DeviceObject.InBound, DeviceObject.OutBound]));
      end;
 
      OutputData.SaveToFile('S:\bandwidth\Bandwidth.txt');
    finally
      OutputData.Free;
    end;
  finally
    HashTable.Free;
  end;
end;

The result is a CSV file ready for excel.