As I promised a long time ago I have to conclude the SimpleStorage description with this final triology post. In this post I will present two very powerfull and complex mechanics behind SimpleStorage:

  • Filters
  • Adapters

Filters

Filters are a very powerful addition to SimpleStorage, that let us “filter” data before that data is written to or read from an element. They are defined on a global scope so when you register them in an application, you have access to them in all units that use SimpleStorage. Implementing them is fairly simple. But first I would like to show how SimpleStorage stores data internally

  // value loading and saving procedures (string and stream)
  TLoadValueAsString = function(const Element: IXMLNode): XmlString;
  TLoadValueAsStream = procedure(const Element: IXMLNode; const Value: TStream);
  TSaveValueAsStream = procedure(const Element: IXMLNode; const Value: TStream);
  TSaveValueAsString = procedure(const Element: IXMLNode; const Value: XmlString);
 
  TStorageData = record
    LoadValueAsStream: TLoadValueAsStream;
    LoadValueAsString: TLoadValueAsString;
    SaveValueAsStream: TSaveValueAsStream;
    SaveValueAsString: TSaveValueAsString;
    ElementNode: IXMLNode;
  end;

This is the core of the single Element. The TStorageData record contains the actual reference to the underlying XML node and it has four data load / save procedures. This means that we can override how simple data types are loaded and saved (all as string eventually) and how complex data types are loaded and saved (all as stream eventually).

If you do not understand the mechanic yet, do not worry, there already is a sample filter included in the package. Compression filter handles on the fly data gzip compression and decompression. I will show how this filter is registered and a sample data save procedure from the same filter.

First the registration of the filter.

  function GetNodeGZipProxy(const ElementNode: IXMLNode): TStorageData;
  begin
    Result.LoadValueAsStream := @LoadNodeValueAsGZipStream;
    Result.LoadValueAsString := @LoadNodeValueAsGZipString;
    Result.SaveValueAsStream := @SaveNodeValueAsGZipStream;
    Result.SaveValueAsString := @SaveNodeValueAsGZipString;
    Result.ElementNode := ElementNode;
  end;
 
  initialization
    RegisterFilter('gzip', @GetNodeGZipProxy);

And a a sample SaveNodeValueAsGZipStream function

procedure SaveNodeValueAsGZipStream(const Element: IXMLNode; const Value: TStream);
var
  CompressedStream: TMemoryStream;
begin
  if Value.Size > 0 then
  begin
    CompressedStream := TMemoryStream.Create;
    try
      ZLibExGZ.GZCompressStream(Value, CompressedStream);
      CompressedStream.Seek(0, soFromBeginning);
 
      // finally write the compressed and encoded string as text
      Element.Text := Base64Encode(CompressedStream)
    finally
      CompressedStream.Free;
    end;
  end;
end;

It is that simple. The whole filter is only 160 lines of code. You could build more complex filters, but the whole idea is the simplicity of it. Now to show how to simply compress and decompress a jpeg image with SimpleStorage

  MS := TMemoryStream.Create;
  try
    Jpeg.SaveToStream(MS);
    MS .Position := 0;
 
    // create the storage and select the single value of interest
    SrcStorage := CreateStorage('BinaryStorage');
    SrcStorage.Ensure('Image').Filter('gzip').AsBinary.LoadFromStream(MS);
    SrcStorage.SaveToFile('Image.xml');
  finally
    MS.Free;
  end;
    SrcStorage := StorageFromFile('Image.xml');
    Jpeg.LoadFromStream(SrcStorage.Get('Image').Filter('gzip').AsBinary.Stream);

Yes it is that simple and compact. If you just want to handle binary data and not use compression or any other filter then SimpleStorage handles that for you out of the box. Here is the IBinary interface that supports handling binary data types.

  IBinary = Interface(IValueData)
  ['{6CD7653A-55F8-477A-9F64-E49131982026}']
    // getters and setters
    function _GetStream: TStream;
    // binary functions and procedures
    procedure SaveToFile(const FileName: string; const FailIfInvalid: Boolean = False);
    procedure LoadFromFile(const FileName: string; const Mode: Word = fmShareDenyNone);
    procedure SaveToStream(const Stream: TStream; const FailIfInvalid: Boolean = False);
    procedure LoadFromBuffer(const Buffer: Pointer; const Size: Cardinal);
    procedure LoadFromElement(const Element: IElement);
    procedure LoadFromStream(const Stream: TStream);
    procedure SaveToBuffer(var Buffer: Pointer);
    procedure LoadFromXML(const XML: XmlString);
    property Stream: TStream read _GetStream;
  end;

Adapters

Now we are left to explain what adapters are. In fact they are very similar to filters, but instead of manipulating the data in some way they “map” the data from XML to some in memory object and vice versa. They are some sort of XML serialization for objects (objects persistence if you will). You have to write the adapter for every object type by hand for now (there is no automation for it yet), but if you do so for objects you use a lot, it pays of eventually.

As I said adapters are very similar to filters and the sample registration proves that.

function GetDataSetAdapterData(const Element: IElement): TAdapterData;
begin
  Result.LoadAdapterData := @LoadAdapterDataAsDataset;
  Result.SaveAdapterData := @SaveAdapterDataAsDataset;
  Result.Element := Element;
end;
 
initialization
  RegisterAdapter('DataSet', @GetDataSetAdapterData);

You have two procedures “LoadAdapterData” and “SaveAdapterData”. They map data from and to XML. To show the sample for the “DataSet” adapter (it is included as a sample adapter in the package) it would be as following

procedure SaveAdapterDataAsDataset(const Element: IElement; const DataObject: TObject);
var
  Node: IElement;
  Field: IElement;
  DataSet: TDataSet;
begin
  DataSet := TDataSet(DataObject);
 
  for Node in Element.Nodes do
  begin
    DataSet.Append;
 
    for Field in Node.Values do
      DataSet.FieldByName(Field.Attributes.Get('Name').AsString).AsVariant := Field.AsString;
 
    DataSet.Post;
  end;
end;

This way you can easily map DataSet to XML and back. Sample code for that would be (load and save)

  ClientDS.Active := True;
  ClientDS.EmptyDataSet;
 
  for I := 1 to 100 do
    ClientDS.InsertRecord([I, 'SomeName', 'SomeData']);
 
  SS := CreateStorage;
  SS.Ensure('MemTable').Adapter('DataSet').Load(ClientDS);
  ClientDS.Active := True;
  ClientDS.EmptyDataSet;
  SS := CreateStorage;
 
  for I := 1 to 100 do
  begin
    NewRecord := SS.Ensure('MemTable').Append('Record');
    NewRecord.Append('Field[@Name="ID"]').AsInteger := I;
    NewRecord.Append('Field[@Name="Name"]').AsString := 'SomeName';
    NewRecord.Append('Field[@Name="Data"]').AsString := 'SomeData';
  end;
 
  SS.Get('MemTable').Adapter('DataSet').Save(ClientDS);

Conclusion

SimpleStorage is a robust and easy way to threat XML as data storage and serialization medium. It simplifies handling with XML as much as possible by adding an upper level to XML operations and by exposing powerful mechanics like filers, adapters and enumerators (combined with XPath).

The code is stable. If you find a bug you can email me and I will fix it. To download the code with the demo get it hereSimpleStorage