Simple XML based storage – Part III.
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 here – SimpleStorage
From Zero To One » Blog Archive » What is new in SimpleStorage wrote,
[...] Simple XML based storage – Part III. [...]
Link | February 7th, 2010 at 2:56 pm
From Zero To One » Blog Archive » Build your XML the LinqToXML style in native code – Part I. wrote,
[...] http://www.cromis.net/blog/2009/07/simple-xml-based-storage-part-iii/ [...]
Link | March 9th, 2010 at 10:13 am