Archive for September, 2007

Easy XML parsing

No Comments »Written on September 29th, 2007 by
Categories: Software Development

I've mentioned my manual xml parsing in Noma before, but i wanted to give a detailed overview of how it works. I think this approach is pretty nice, and should be very easy to reuse. First of all, let me restate what i needed before i started working on it and why i ultimately chose this solution.

I needed to parse nhibernate mapping files (which have to conform to the nhibernate-mapping.xsd schema) and i wanted all of the data to be available in an easy to use object model. The easiest approach would be to generate classes based on the nhibernate-mapping.xsd through the xsd.exe tool. Then it would simply be a matter of deserializing the nhibernate mapping file and you'd have your data available. Unfortunately, using classes generated by xsd.exe is a pain and i'd only consider it in the case of a very simple schema. The nhibernate mapping schema is very extensive and anything but simple so i didn't go for this approach. I could also let xsd.exe generate a DataSet based on the schema. I'll go out of my way to avoid DataSets even when i get paid to use them so i sure as hell won't use them for a project i'm working on in my spare time. Another interesting approach was offered by a coworker of mine. The idea was to basically use xslt to transform the hibernate mapping xml data to another xml format that i could simply deserialize into my own object model. That is a very interesting approach, but i don't like using xslt so i didn't go for this approach either.

I didn't really find another good approach so i just started parsing the xml files manually. It was a pain at first and the work progressed very slowly. But i had my tests to back me up, so i was often refactoring my parsing code to make it more reusable and to allow me to write less code as i went along to parse different kinds of xml elements. The result is a generic NodeParser class which really makes it easy to do your own xml parsing. If you ever need to parse xml files and you don't have control over the schema, this approach is definitely worthy of consideration.

It all starts with the following abstract base class:

   public abstract class NodeParser<T>

This class defines the following abstract method:

      public abstract T ParseNode(XmlNode node);

If you need to parse a specific xml element, simply inherit from this class and pass the type of the resulting object (the one that will hold the data offered by the xml element) as the generic parameter. You will then implement the ParseNode method so it will create an instance of the type you defined and pass it the data contained in the given XmlNode. The NodeParser class provides several protected methods to make it very easy to retrieve the data from the XmlNode.

The first thing i wanted was a way to retrieve the value of an attribute of the element, so the following protected method is provided:

      protected string GetAttributeValue(XmlNode node, string attribute, string defaultValue)
      {
         if (node.Attributes[attribute] == null)
         {
            return defaultValue;
         }
 
         return node.Attributes[attribute].Value;
      }

As you can see, there's not much to it. But you obviously don't want to do this each an every time you need an attribute's value so you can just use this method. Specific overloads also exist to retrieve boolean or numeric attribute values instead of string values.

That takes care of retrieving simple values of attributes. The nhibernate mapping xsd defines a lot of enumerations though, so i needed something easy to retrieve those values as well. Basically i just wanted a method where i could pass the xml attribute containing the enumeration value as a string, and it would return my own custom enumeration value. The following method provides that ability:

      protected TValue GetAttributeValue<TValue>(XmlNode node, string attribute, TValue defaultValue,
                                                 ValueMapper<TValue> mapper)
      {
         string value = GetAttributeValue(node, attribute, (string)null);
 
         if (value == null)
         {
            return defaultValue;
         }
 
         return mapper.GetValue(value);
      }

More information about the ValueMapper type can be found here. This allows me to retrieve whatever enumerated value i want to, provided that i have a class derived from ValueMapper which maps the string values to the specific enumerated values.

So what do we have now? A very easy way to retrieve the value of an attribute in any way we want to: a string, a numeric value, a boolean, or an enumerated value. If we need support for more types (dates for instance), it'll be easy to add another overload for that specific type. We can get all of the values of all the attributes of an xml element very easily now. Now let's move on to child elements and collections of child elements.

The following methods provide a way to retrieve one child node, or a list of child nodes based on an xpath query:

      protected XmlNodeList FindChildNodes(XmlNode parentNode, string xpath)
      {
         return parentNode.SelectNodes(xpath, GetNamespaceManager(parentNode));
      }
 
      protected XmlNode FindChildNode(XmlNode parentNode, string xpath)
      {
         return parentNode.SelectSingleNode(xpath, GetNamespaceManager(parentNode));
      }
 
      private static XmlNamespaceManager GetNamespaceManager(XmlNode node)
      {
         XmlNamespaceManager nsManager = new XmlNamespaceManager(node.OwnerDocument.NameTable);
         nsManager.AddNamespace(Constants.XmlNamespacePrefix, Constants.XmlNamespace);
 
         return nsManager;
      }

Even though xpath is very powerful, i don't really like using it so if i can hide it in a method, i surely will:

      protected XmlNode GetChildNodeOfType(string elementName, XmlNode parentNode)
      {
         return FindChildNode(parentNode, "./" + Constants.XmlNamespacePrefix + ":" + elementName);
      }
 
      protected XmlNodeList GetChildNodesOfType(string elementName, XmlNode parentNode)
      {
         return FindChildNodes(parentNode, "./" + Constants.XmlNamespacePrefix + ":" + elementName);
      }

So now i can use these methods to easily retrieve a child element, or a list of elements simply by passing the parent xml node and the name of the element(s) i'm looking for. That's nice, but then i still need to convert that element to a typed object, or perhaps a list of typed objects. Fortunately, i can reuse the NodeParser type for this:

      protected TObject GetObjectFromNode<TObject>(string elementType, XmlNode node,
                                                   NodeParser<TObject> nodeParser)
      {
         XmlNode objectNode = GetChildNodeOfType(elementType, node);
 
         if (objectNode == null)
         {
            return default(TObject);
         }
 
         return nodeParser.ParseNode(objectNode);
      }

This method will return a typed object from the xml node passed into it, based on the type of NodeParser passed in to it. A NodeParser could very well contain other NodeParser types to parse specific child elements. These contained NodeParsers can then be passed to this method to retrieve other typed objects. This might seem weird at first, but you'll see an example of this later on which will make it clear.

One more thing i needed was an easy way to retrieve a whole collection of typed objects representing multiple child elements within an xml node. We can use the same trick we used in the GetObjectFromNode method for this:

      protected Dictionary<string, TObject> CreateDictionaryOfObjectsInNode<TObject>(string elementType,
                                                                                     XmlNode node,
                                                                                     NodeParser<TObject>
                                                                                        nodeParser)
         where TObject : INamedMapping
      {
         Dictionary<string, TObject> objects = new Dictionary<string, TObject>();
 
         foreach (XmlNode objectNode in GetChildNodesOfType(elementType, node))
         {
            TObject typedObject = nodeParser.ParseNode(objectNode);
            objects.Add(typedObject.Name, typedObject);
         }
 
         return objects;
      }

This method returns a dictionary containing typed objects representing all of the child elements in the parent xml node that match the given elementType. And obviously, it uses another typed NodeParser to parse those child elements.

So far, all of this has been pretty abstract and i can imagine it'll be a lot clearer once you take a look at the following examples.

Let's start with a simple one:

   public class ManyToManyMappingParser : NodeParser<ManyToManyMapping>
   {
      public override ManyToManyMapping ParseNode(XmlNode node)
      {
         return new ManyToManyMapping(
            GetAttributeValue(node, Attributes.Class, DefaultValues.ManyToManyClassName),
            GetAttributeValue(node, Attributes.Column, DefaultValues.ManyToManyColumnName),
            GetAttributeValue<FetchMode>(node, Attributes.Fetch, DefaultValues.ManyToManyFetchMode,
                                         MapperProvider.FetchModeMapper),
            GetAttributeValue<NotFoundAction>(node, Attributes.NotFound, DefaultValues.ManyToManyNotFoundAction,
                                              MapperProvider.NotFoundActionMapper),
            GetAttributeValue(node, Attributes.Where, DefaultValues.ManyToManyWhereClause));
      }
   }

Now, this NodeParser will parse nhibernate's many-to-many element and convert it to a ManyToManyMapping instance. As you can see, 3 simple attribute values are retrieved (Class, Column and Where), together with 2 enumerated values (FetchMode and NotFoundAction). As you can see, this is not a lot of code and is very readable.

The many-to-many element is a possible child element of a list mapping. So let's see how a list element is parsed:

   public class ListMappingParser : NodeParser<ListMapping>
   {
      private readonly CompositeElementMappingParser _compositeElementParser = new CompositeElementMappingParser();
      private readonly ElementMappingParser _elementMappingParser = new ElementMappingParser();
      private readonly IndexMappingParser _indexMappingParser = new IndexMappingParser();
      private readonly ManyToManyMappingParser _manyToManyMappingParser = new ManyToManyMappingParser();
      private readonly OneToManyMappingParser _oneToManyMappingParser = new OneToManyMappingParser();
 
      public override ListMapping ParseNode(XmlNode node)
      {
         return new ListMapping(
            GetAttributeValue<Access>(node, Attributes.Access, DefaultValues.ListAccess, MapperProvider.AccessMapper),
            GetAttributeValue(node, Attributes.BatchSize, DefaultValues.ListBatchSize),
            GetAttributeValue<Cascade>(node, Attributes.Cascade, DefaultValues.ListCascade,
                                       MapperProvider.CascadeMapper),
            GetAttributeValue(node, Attributes.Check, DefaultValues.ListCheck),
            GetObjectFromNode<CompositeElementMapping>(Elements.CompositeElement, node, _compositeElementParser),
            GetObjectFromNode<ElementMapping>(Elements.Element, node, _elementMappingParser),
            GetAttributeValue<FetchMode>(node, Attributes.Fetch, DefaultValues.ListFetchMode,
                                         MapperProvider.FetchModeMapper),
            GetAttributeValue(node, Attributes.Generic, DefaultValues.ListGeneric),
            GetObjectFromNode<IndexMapping>(Elements.Index, node, _indexMappingParser),
            GetAttributeValue(node, Attributes.Inverse, DefaultValues.ListInverse),
            GetAttributeValueFromChildElement(Elements.Key, Attributes.Column, node),
            GetObjectFromNode<ManyToManyMapping>(Elements.ManyToMany, node, _manyToManyMappingParser),
            GetAttributeValue(node, Attributes.Name, DefaultValues.ListName),
            GetObjectFromNode<OneToManyMapping>(Elements.OneToMany, node, _oneToManyMappingParser),
            GetAttributeValue(node, Attributes.Persister, DefaultValues.ListPersister),
            GetAttributeValue(node, Attributes.OptimisticLock,
                              DefaultValues.ListReAcquireOptimisticLockWhenDirty),
            GetAttributeValue(node, Attributes.Schema, DefaultValues.ListSchema),
            GetAttributeValue(node, Attributes.Table, DefaultValues.ListTableName),
            GetAttributeValue(node, Attributes.Where, DefaultValues.ListWhere));
      }
   }

Whoa, that's a lot all of a sudden! But when you think about the complexity of the list element in an nhibernate mapping file, this is actually not a lot of code. This specific NodeParser contains references to other NodeParsers to parse ElementMappings, CompositeElementMappings, IndexMappings, ManyToManyMappings and ManyToOneMappings. All of which are possible child elements of a list mapping. Take a look at the calls to GetObjectFromNode. By passing along a NodeParser to parse a specific child element, the GetObjectFromNode method can retrieve the typed mapping object you want. And all you needed to do was call GetObjectFromNode. Well you also had to develop the specific NodeParser class but at least you can reuse that one later on. Anyway, i hope the usage of GetObjectFromNode is now very clear.

I still haven't shown you an example of the CreateDictionaryOfObjectsInNode method being used so here's one:

   public class CompositeElementMappingParser : NodeParser<CompositeElementMapping>
   {
      private readonly ManyToOneMappingParser _manyToOneMappingParser = new ManyToOneMappingParser();
      private readonly PropertyMappingParser _propertyMappingParser = new PropertyMappingParser();
 
      public override CompositeElementMapping ParseNode(XmlNode node)
      {
         return new CompositeElementMapping(
            GetAttributeValue(node, Attributes.Class, DefaultValues.CompositeElementClassName),
            GetAttributeValueFromChildElement(Elements.Parent, Attributes.Name, node),
            CreateDictionaryOfObjectsInNode<ManyToOneMapping>(Elements.ManyToOne, node, _manyToOneMappingParser),
            CreateDictionaryOfObjectsInNode<PropertyMapping>(Elements.Property, node, _propertyMappingParser));
      }
   }

A composite element mapping can contain one or more property mappings, and one or more many-to-one mappings. With the little bit of code shown above we can parse the entire node and convert it to a typed object that will be easy to use.

I hope the entire usage of the NodeParser class is clear and i hope i demonstrated that doing your own xml parsing doesn't have to be that cumbersome. It's still a lot more work than simply deserializing into classes generated by xsd.exe but i think the end result of this approach is a lot better.

Noma’s hibernate mapping parsing

2 commentsWritten on September 23rd, 2007 by
Categories: Uncategorized

Time for a little status update. I'm still working on parsing the hibernate mapping files but the end is finally near. The following elements are now supported: array, bag, class, column, composite-element, discriminator, element, generator, hibernate-mapping, id, index, index-many-to-many, key, list, many-to-many, many-to-one, map, one-to-many, one-to-one, parent, property, set, timestamp and version. Supported meaning that these elements are parsed from xml and that there are easy to use classes for all of these elements and the data they hold. Not supported yet: any, composite-id, component, dynamic-component, idbag, import, joined-subclass, primitive-array and subclass. Support for these will be added soon. If you can think of an element i forgot, let me know so i can include it.

Now, some of these elements are parsed completely (meaning each attribute and child element), and some of them are only being parsed partially (meaning only the attributes and child elements that i think are relevant to Noma). Elements and attributes that my parser doesn't understand are simply ignored. But the parser is very easy to extend and/or modify so adding support for other elements/attributes is fairly easy and not a lot of work... but it's boring work though :) (which is why it's progressing so slowly)

Once the parser is finished, i need to gather metadata from your entity classes and also provide all of that information in an easy to use object model. That should be fairly easy since it'll just use reflection to inspect all of the types the classes hold. And then, i'll finally be able to start working on the rule engine and the whole point of this stuff: the rules :)

If you want, you can always browse the code online or get it all through subversion.

I'll try to post a weekly status update on Noma from now on... Not sure if this is interesting to any of you, but hopefully it'll force me to keep spending time on it :P

Read-Only Generic Dictionary

2 commentsWritten on September 23rd, 2007 by
Categories: Software Development

I can't believe the .NET framework does not include a read-only dictionary class. I love dictionaries, but i'm always hesitant to expose dictionaries as properties because i don't like consumers of my class having the ability to add or remove items from them directly. I never expose lists either... i'll always expose them as a ReadOnlyCollection. So anyway, here's a read only dictionary class. It would've been so much nicer if i could just inherit from Dictionary and override the few methods i needed to 'block' but obviously us dumb non-microsoft programmers can't be trusted to know what the hell we're doing so microsoft chose not to declare the interesting methods as virtual...

    [Serializable]
    public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, ISerializable,
                                                    IDeserializationCallback
    {
        private const string _readOnlyExceptionMessage = "This Dictionary is read-only!";
 
        private readonly IDictionary<TKey, TValue> _wrappedDictionary;
 
        public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionaryToWrap)
        {
            _wrappedDictionary = dictionaryToWrap;
        }
 
        #region IDeserializationCallback Members
 
        public void OnDeserialization(object sender)
        {
            ((IDeserializationCallback)_wrappedDictionary).OnDeserialization(sender);
        }
 
        #endregion
 
        #region IDictionary Members
 
        public void Add(object key, object value)
        {
            throw new Exception(_readOnlyExceptionMessage);
        }
 
        public bool Contains(object key)
        {
            return ((IDictionary)_wrappedDictionary).Contains(key);
        }
 
        IDictionaryEnumerator IDictionary.GetEnumerator()
        {
            return ((IDictionary)_wrappedDictionary).GetEnumerator();
        }
 
        public bool IsFixedSize
        {
            get { return ((IDictionary)_wrappedDictionary).IsFixedSize; }
        }
 
        ICollection IDictionary.Keys
        {
            get { return ((IDictionary)_wrappedDictionary).Keys; }
        }
 
        public void Remove(object key)
        {
            throw new Exception(_readOnlyExceptionMessage);
        }
 
        ICollection IDictionary.Values
        {
            get { return ((IDictionary)_wrappedDictionary).Values; }
        }
 
        public object this[object key]
        {
            get { return ((IDictionary)_wrappedDictionary)[key]; }
            set { throw new Exception(_readOnlyExceptionMessage); }
        }
 
        public void CopyTo(Array array, int index)
        {
            ((IDictionary)_wrappedDictionary).CopyTo(array, index);
        }
 
        public bool IsSynchronized
        {
            get { return ((IDictionary)_wrappedDictionary).IsSynchronized; }
        }
 
        public object SyncRoot
        {
            get { return ((IDictionary)_wrappedDictionary).SyncRoot; }
        }
 
        #endregion
 
        #region IDictionary<TKey,TValue> Members
 
        public void Add(TKey key, TValue value)
        {
            throw new Exception(_readOnlyExceptionMessage);
        }
 
        public bool ContainsKey(TKey key)
        {
            return _wrappedDictionary.ContainsKey(key);
        }
 
        public ICollection<TKey> Keys
        {
            get { return new List<TKey>(_wrappedDictionary.Keys).AsReadOnly(); }
        }
 
        public bool Remove(TKey key)
        {
            throw new Exception(_readOnlyExceptionMessage);
        }
 
        public bool TryGetValue(TKey key, out TValue value)
        {
            return _wrappedDictionary.TryGetValue(key, out value);
        }
 
        public ICollection<TValue> Values
        {
            get { return new List<TValue>(_wrappedDictionary.Values).AsReadOnly(); }
        }
 
        public TValue this[TKey key]
        {
            get { return _wrappedDictionary[key]; }
            set { throw new Exception(_readOnlyExceptionMessage); }
        }
 
        public void Add(KeyValuePair<TKey, TValue> item)
        {
            throw new Exception(_readOnlyExceptionMessage);
        }
 
        public void Clear()
        {
            throw new Exception(_readOnlyExceptionMessage);
        }
 
        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            return _wrappedDictionary.Contains(item);
        }
 
        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            _wrappedDictionary.CopyTo(array, arrayIndex);
        }
 
        public int Count
        {
            get { return _wrappedDictionary.Count; }
        }
 
        public bool IsReadOnly
        {
            get { return true; }
        }
 
        public bool Remove(KeyValuePair<TKey, TValue> item)
        {
            throw new Exception(_readOnlyExceptionMessage);
        }
 
        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return _wrappedDictionary.GetEnumerator();
        }
 
        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IDictionary)_wrappedDictionary).GetEnumerator();
        }
 
        #endregion
 
        #region ISerializable Members
 
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            ((ISerializable)_wrappedDictionary).GetObjectData(info, context);
        }
 
        #endregion
    }

How TDD leads to good code, Part 2

No Comments »Written on September 19th, 2007 by
Categories: testing

Like i said, it's moving along pretty fast now :)

parsing an id and its generator element:

    public class GeneratorMappingParser : NodeParser<GeneratorMapping>
    {
        public override GeneratorMapping ParseNode(XmlNode node)
        {
            Dictionary<string, string> parameters = GetParametersFromGeneratorElementIfPresent(node);
 
            GeneratorMapping generatorMapping =
                new GeneratorMapping(
                    GetAttributeValueAsGeneratorClass(node, HbmAttributes.Class, HbmDefaultValues.GeneratorClass),
                    parameters);
 
            return generatorMapping;
        }
 
        private Dictionary<string, string> GetParametersFromGeneratorElementIfPresent(XmlNode node)
        {
            Dictionary<string, string> parameters = new Dictionary<string, string>();
 
            foreach (XmlNode childNode in GetChildNodesOfType(HbmElements.Param, node))
            {
                parameters.Add(childNode.Attributes[HbmAttributes.Name].Value, childNode.InnerText);
            }
 
            return parameters;
        }
    }


    public class IdMappingParser : NodeParser<IdMapping>
    {
        private readonly GeneratorMappingParser _generatorMappingParser;
 
        public IdMappingParser()
        {
            _generatorMappingParser = new GeneratorMappingParser();
        }
 
        public override IdMapping ParseNode(XmlNode node)
        {
            IdMapping idMapping =
                new IdMapping(
                    GetAttributeValue(node, HbmAttributes.Name, HbmDefaultValues.IdName),
                    GetAttributeValueAsType(node, HbmAttributes.Type, HbmDefaultValues.IdType),
                    GetAttributeValue(node, HbmAttributes.Column, HbmDefaultValues.IdColumn),
                    GetAttributeValueAsUnsavedValue(node, HbmAttributes.UnsavedValue,
                                                          HbmDefaultValues.IdUnsavedValue),
                    GetAttributeValueAsAccess(node, HbmAttributes.Access, HbmDefaultValues.IdAccess));
 
            AddGeneratorIfPresentInNode(idMapping, node);
 
            return idMapping;
        }
 
        private void AddGeneratorIfPresentInNode(IdMapping idMapping, XmlNode node)
        {
            XmlNode generatorNode = GetChildNodeOfType(HbmElements.Generator, node);
 
            if (generatorNode != null)
            {
                idMapping.GeneratorMapping = _generatorMappingParser.ParseNode(generatorNode);
            }
        }
    }

And the ParseNode method of the ClassMappingParser now looks like this:

        public ClassMapping ParseNode(XmlNode node, MappingSettings settings)
        {
            ClassMapping classMapping = CreateClassMapping(settings, node);
 
            AddMappedId(classMapping, node);
            AddMappedProperties(classMapping, node);
 
            return classMapping;
        }

With the AddMappedId method being as simple as this:

        private void AddMappedId(ClassMapping classMapping, XmlNode node)
        {
            XmlNode idNode = GetChildNodeOfType(HbmElements.Id, node);
 
            if (idNode != null)
            {
                classMapping.IdMapping = _idMappingParser.ParseNode(idNode);
            }
        }

All covered by tests of course :)

In case you were wondering: no, i won't post every little bit of code i write for noma but i do hope you get the point about TDD leading to good code.

How TDD leads to good code

No Comments »Written on September 19th, 2007 by
Categories: testing

For Noma, i need to parse NHibernate mapping files. As i mentioned earlier, i decided to do the parsing myself instead of reusing the NHibernate code. I'm doing this the TDD way, so i'm always doing one little thing at a time... write a little test, code a little to make the test work. At first, you progress slowly. Very slowly even. I was moving along so slow i actually considered simply generating some dumb classes based on the nhibernate mapping xsd and then simply convert that to my own easy to use object model representing the mappings. But one look at those generated classes made me think otherwise. So i figured i'd bite the bullet and keep on working the way i was. Since i was doing one little thing at a time, and i was writing my tests first, everything i wrote was very easy to reuse and now, i'm moving along a lot more rapidly. And the best part is that i can run my test suite with every change to make sure i didn't break anything.

Let me show you a small example... parsing property mappings in a class definition is done by this part of code:

    public class PropertyMappingParser : NodeParser<PropertyMapping>
    {
        public override PropertyMapping ParseNode(XmlNode node)
        {
            PropertyMapping propertyMapping =
                new PropertyMapping(
                    GetAttributeValue(node, HbmAttributes.Name, HbmDefaultValues.PropertyName),
                    GetAttributeValue(node, HbmAttributes.Column, HbmDefaultValues.PropertyColumnName),
                    GetAttributeValueAsType(node, HbmAttributes.Type, HbmDefaultValues.PropertyType),
                    GetAttributeValueAsBoolean(node, HbmAttributes.Update, HbmDefaultValues.PropertyIncludeInUpdate),
                    GetAttributeValueAsBoolean(node, HbmAttributes.Insert, HbmDefaultValues.PropertyIncludeInInsert),
                    GetAttributeValue(node, HbmAttributes.Formula, HbmDefaultValues.PropertyFormula),
                    GetAttributeValueAsAccess(node, HbmAttributes.Access, HbmDefaultValues.PropertyAccess),
                    GetAttributeValueAsBoolean(node, HbmAttributes.OptimisticLock,
                                               HbmDefaultValues.PropertyReAcquireOptimisticLockWhenDirty),
                    GetAttributeValueAsGeneratedValue(node, HbmAttributes.Generated, HbmDefaultValues.PropertyGeneratedValue));
 
            return propertyMapping;
        }
    }

The PropertyMappingParser is used by the ClassMappingParser like this:

        public ClassMapping ParseNode(XmlNode node, MappingSettings settings)
        {
            ClassMapping classMapping = CreateClassMapping(settings, node);
 
            AddMappedProperties(classMapping, node);
 
            return classMapping;
        }


        private void AddMappedProperties(ClassMapping classMapping, XmlNode node)
        {
            foreach (XmlNode propertyNode in GetChildNodesOfType(HbmElements.Property, node))
            {
                classMapping.AddPropertyMapping(_propertyMappingParser.ParseNode(propertyNode));
            }
        }

I'd say that code is easy to understand and very maintanable. Parsing all the other elements i need to parse should move along pretty fast now.