Difference between revisions of "Patterns"
(→Merging streams) |
(→Manipulating proxy input streams) |
||
Line 292: | Line 292: | ||
Using '''xml::ximultistream''' enables interesting use cases for : | Using '''xml::ximultistream''' enables interesting use cases for : | ||
− | + | * falling back on a piece of data | |
− | + | * overriding a piece of data | |
− | + | ||
Here is an example of data fallback : | Here is an example of data fallback : |
Revision as of 14:48, 23 June 2009
This section gathers examples from various use cases.
It provides tricks in order to better grasp the flexibility and expressiveness of the library.
XML
Beautifying an existing file
An XML file can very easily be cleaned up by simply loading and writing it back :
xml::xifstream xis( "input.xml" ); xml::xofstream xos( "output.xml" ); xos << xis;
However there is no means to query and copy the encoding.
Handling different documents
The xml::list feature can be used in order to provide a switch-like structure to process several documents with different root elements.
Given for example the following documents :
<document-1> <element-1>content</element-1> </document-1>
<document-2> <element-2 /> </document-2>
<document-3> <element-3 attribute="something" /> </document-3>
A code able to unserialize all of them from a single entry point could be :
class my_class { public: my_class( xml::xistream& xis ) { xis >> xml::list( "document-1", *this, &my_class::read_document_1 ) >> xml::list( "document-2", *this, &my_class::read_document_2 ) >> xml::list( "document-3", *this, &my_class::read_document_3 ); } private: void read_document_1( xml::xistream& xis ) { std::string content; xis >> xml::content( "element-1", content ); } void read_document_2( xml::xistream& xis ) { xis >> xml::start( "element-2" ) >> xml::end; } void read_document_3( xml::xistream& xis ) { std::string attribute; xis >> xml::attribute( "attribute", attribute ); } };
Factorizing the unserialization of similar nodes
If different nodes have elements and/or attributes in common, factorization can sometimes significantly reduce the amount of code.
Most of the time the benefits are better when the code logics behind those elements indicate that they are very tied together.
For example given the following document :
<document> <element-1 name="name 1">content</element-1> <element-2 name="name 2"/> <element-3 name="name 3" attribute="something" /> </document-3>
A possible factorization can be :
class my_class { public: my_class( xml::xistream& xis ) { process( xis, "element-1", &my_class::read_element_1 ); process( xis, "element-2", &my_class::read_element_2 ); process( xis, "element-3", &my_class::read_element_3 ); } private: template< typename T > void process( xml::xistream& xis, const std::string& element, T functor ) const { std::string name; xis >> xml::start( element ) >> xml::attribute( "name", name ); (this->*functor)( xis, name ); xis >> xml::end; } void read_element_1( xml::xistream& xis, const std::string& name ) { std::string content; xis >> content; } void read_element_2( xml::xistream& xis, const std::string& name ) { } void read_element_3( xml::xistream& xis, const std::string& name ) { std::string attribute; xis >> xml::attribute( "attribute", attribute ); } };
Note the use of a template helper method which allows the user not to have to worry about the exact type of the functor.
Creating objects depending on the type of a node
Using the factory design pattern decouples concrete object instantiation from object manipulation.
For example if a collection of objects must be instantiated from different concrete classes implementing the same interface, the factory definition may look like :
class my_factory { public: virtual my_interface* create( const std::string& type, xml::xistream& xis ) = 0; };
The following code demonstrates how to create all objects from the collection with the factory and add them to a vector :
class my_class { public: my_class( my_factory& factory ) : factory_( factory ) {} void load( xml::xistream& xis ) { xis >> xml::list( *this, &my_class::create ); } private: void create( const std::string& type, xml::xistream& xis ) { collection_.push_back( factory_.create( type, xis ) ); } private: my_factory& factory_; std::vector< my_interface* > collection_; };
The implementation of the factory requires testing the type of the element to be created, for example :
class my_factory_imp : public my_factory { public: virtual interface* create( const std::string& type, xml::xistream& xis ) { if( type == "first_type" ) return new first_type( xis ); if( type == "second_type" ) return new second_type( xis ); if( type == "third_type" ) return new third_type( xis ); return 0; } };
Moving the instantiation code to a factory provides better extensibility.
Adding support for new types is thus possible without changing the existing code, only by extending the factory, for example :
class my_extended_factory_imp : public my_factory_imp { public: virtual interface* create( const std::string& type, xml::xistream& xis ) { if( type == "extended_type" ) return new extended_type( xis ); if( type == "another_extended_type" ) return new another_extended_type( xis ); return my_factory_imp::create( type, xis ); } };
Merging streams
Assuming the goal is to load from files two documents with the same root node and to merge them.
The most straightforward way to implement this would probably be :
xml::xifstream xis_1( filename_1 ); xis_1 >> xml::start( "root" ); xml::xifstream xis_2( filename_2 ); xis_2 >> xml::start( "root" ); xml::xofstream xos( filename ); xos << xml::start( "root" ) << xis_1 << xis_2 << xml::end;
Note that if the root element has attributes, they will be appended to the root of the output.
The attributes from xis_2 root with the same names as the ones from xis_1 root will then overwrite them.
Another way to obtain the same effect would be :
xml::xifstream xis_1( filename_1 ); xml::xifstream xis_2( filename_2 ); xml::ximultistream xis( xis_1, xis_2 ); xis >> xml::start( "root" ); xml::xofstream xos( filename ); xos << xml::content( "root", xis );
The later solution further decouples the writing from the reading, which can be beneficial in some use cases.
Turning a non-constant input stream into constant
The xml::xisubstream implementation has proven to be useful because its constructor takes a constant reference to an xml::xistream.
Therefore instead of the following code :
class my_class { public: void process( xml::xistream& xis ) const { // read from xis } };
It is possible to write a method taking a constant reference to the stream and still be able to read from it :
class my_class { public: void process( const xml::xistream& xis ) const { xml::xisubstream xiss( xis ); // read from xiss } };
Or even directly :
class my_class { public: void process( xml::xisubstream xiss ) const { // read from xiss } };
Because the constructor for xml::xisubstream is deliberately not declared explicit.
What is then even more interesting is the possibility to create the xml::xistream as a temporary variable at the method call location :
my_class my_instance; my_instance.process( xml::xifstream( "my_file.xml" ) );
Using Boost.Bind
Boost.Bind can be used to create a functor for calling xml::list (or xml::attributes) :
class my_class { public: void my_method( xml::xistream& xis ) { // read from xis } };
With the actual unserialization code being :
my_class my_instance; xis >> xml::start( "root" ) >> xml::list( "element", boost::bind( &my_class::my_method, &my_instance, _1 );
In case of using the alternate version of xml::list without filtering based on the name of the node :
class my_class { public: void my_method( const std::string& name, xml::xistream& xis ) { // read from xis } };
The code would be :
my_class my_instance; xis >> xml::start( "root" ) >> xml::list( boost::bind( &my_class::my_method, &my_instance, _1, _2 ) );
Manipulating proxy input streams
Using xml::ximultistream enables interesting use cases for :
- falling back on a piece of data
- overriding a piece of data
Here is an example of data fallback :
xml::xostringstream xos; xos << xml::content( "element", xml::attribute( "mandatory", false ) ); xml::xistringstream xiss( xos.str() ); xml::ximultistream xims( xis, xiss ); bool mandatory; xims >> xml::start( "element" ) >> xml::attribute( "mandatory", mandatory );
If xis does not have an element node and/or a mandatory attribute the reading won't fail but use the branch provided by xiss.
An example of data override can simply be achieved by inverting the streams when creating the xml::ximultistream in the previous example :
xml::ximultistream xims( xiss, xis );
This way whatever contains xis is ignored if xiss provides the data first.
Buffering creation data
This use case demonstrates how to implement a prototype pattern variation based on XML definitions, for instance using the following data :
<types> <type name="first"> <some-data/> </type> <type name="second"> <some-other-data/> </type> </types>
Each type can be buffered in an xml::xibufferstream stored in an std::map :
typedef std::map< std::string, xml::xistream* > my_types;
The prototype class loading the types into the map could look like :
class my_prototype { public: explicit my_prototype( xml::xistream& xis ) { xis >> xml::list( "type", *this, &my_prototype::load ); } ~my_prototype() { for( my_types::const_iterator it = types_.begin(); it != types_.end(); ++it ) delete it->second; } private: void load( xml::xistream& xis ) { const std::string name = xml::attribute< std::string >( xis, "name" ); xml::xistream*& buffer = types_[name]; if( buffer ) xis.error( "type '" + name + "' already registered" ); buffer = new xml::xibufferstream( xis ); } my_types types_; };
Note that the destructor takes care of deallocating the buffer streams (in real production code some sort or smart pointers would probably be used).
Also for completeness duplicates are tested to prevent memory leaks when overwriting entries in the map.
Next all that is left is to implement the creation method itself, which is straight-forward coupled to a factory like the following :
class my_factory { public: my_interface* create( const std::string& type, xml::xistream& xis ) { if( type == "first" ) return new First( xis ); if( type == "second" ) return new Second( xis ); xis.error( "unknown type '" + type + "'" ); } };
Adding a creation method to the prototype will provide the missing link :
class my_prototype { public: explicit my_prototype( xml::xistream& xis ) { xis >> xml::list( "type", *this, &my_prototype::load ); } ~my_prototype() { for( my_types::const_iterator it = types_.begin(); it != types_.end(); ++it ) delete it->second; } my_interface* create( const std::string& type ) { my_types::const_iterator it = types_.find( type ); if( it == types_.end() ) throw std::runtime_error( "unknown type '" + type + "'" ); return factory_.create( it->second ); } private: void load( xml::xistream& xis ) { const std::string name = xml::attribute< std::string >( xis, "name" ); xml::xistream*& buffer = types_[name]; if( buffer ) xis.error( "type '" + name + "' already registered" ); buffer = new xml::xibufferstream( xis ); } my_types types_; };
Then the prototype would be used like this :
xml::xifstream xis( "types.xml" ); my_prototype prototype( xis ); std::auto_ptr< my_interface > first1( prototype.create( "first" ); std::auto_ptr< my_interface > first2( prototype.create( "first" ); std::auto_ptr< my_interface > second( prototype.create( "second" );
XSL
Performing a file to file transformation
The most simple way to perform a file to file transformation based on a given stylesheet is :
xsl::xftransform xst( "stylesheet.xsl", "output" ); xst << xml::xifstream( "input.xml" );
Merging several xml files before applying a transformation
Combining several XML documents before applying a transformation can be used to dynamically configure the transformation.
It has proved to be useful when xsl::parameter cannot be used because of the complexity of the customization required.
For instance given the following input.xml file :
<elements> <element id="1" type="type1"/> <element id="2" type="type2"/> <element id="3" type="type3"/> </elements>
And the following parameters.xml file :
<types> <type name="type1"> <sub-elements> <sub-element name="sub1"/> </sub-elements> </type> <type name="type2"> <sub-elements> <sub-element name="sub2"/> <sub-element name="sub3"/> </sub-elements> </type> </types>
They can be combined in order to apply the following stylesheet.xsl :
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/root/elements"> <xsl:copy> <xsl:apply-templates select="element"/> </xsl:copy> </xsl:template> <xsl:template match="element"> <xsl:copy> <xsl:variable name="type" select="@type"/> <xsl:copy-of select="@id"/> <xsl:copy-of select="/root/types/type[@name=$type]/sub-elements"/> </xsl:copy> </xsl:template> </xsl:stylesheet>
Using to the following code :
xsl::xftransform xst( "stylesheet.xsl", "output.xml" ); xst << xml::start( "root" ) << xml::xifstream( "input.xml" ) << xml::xifstream( "parameters.xml" ) << xml::end;
Yields the following output.xml :
<elements> <element id="1"> <sub-element name="sub1"/> </element> <element id="2"> <sub-element name="sub2"/> <sub-element name="sub3"/> </element> <element id="3"> <sub-element name="sub1"/> </element> </elements>