Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

Parser Schemas– Easy and Powerful parsing of XML-based languages

, 18 Oct 2005
An article on parsing XML files according to the specified schema.
/***********************************************************************\
 * Comnicate.CodeDom.Xml.ParserSchemas                                 *
 * Parses xml-based languages according to a user defined schema.      *
 * Copyright � 2005 Tomas Deml (as Comnicate!)                         *
 *                  tomasdeml@msn.com                                  *
 *                                                                     *
 * This library is free software; you can redistribute it and/or       *
 * modify it under the terms of the GNU Lesser General Public          *
 * License as published by the Free Software Foundation; either        *
 * version 2.1 of the License, or (at your option) any later version.  *
 *                                                                     *
 * This library is distributed in the hope that it will be useful,     *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of      *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU   *
 * Lesser General Public License for more details.                     *
\***********************************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;
using Comnicate.CodeDom.Xml.ParserSchemas.Rules.Evaluated;
using Comnicate.CodeDom.Xml.ParserSchemas.Rules.Evaluation;
using System.Xml;
using Comnicate.CodeDom.Xml.ParserSchemas.Rules;
using System.Collections.Specialized;

namespace Comnicate.CodeDom.Xml.ParserSchemas.Rules
{
    /// <summary>
    /// Represents the abstract class for all rules supporting child rules evaluation.
    /// </summary>
    public abstract class ParentalRule : EvaluableRule<EvaluatedParentalRule>, IOptionalEntriesSupporter, ILinkRegistry
    {
        #region Fields

        /// <summary>
        /// Separator for separating rule number from rule name.
        /// </summary>
        public const string EvaluatedRuleNumberSeparator = "__";

        // Required children
        private Dictionary<string, Rule> requiredChildren;

        // Optional children
        private Dictionary<string, Rule> optionalChildren;

        // Optional entries match option
        private OptionalEntriesMatchOption optionalMatchOption;

        // Maximum optional entries to match count
        private int maximumOptionalEntriesToMatch;

        // Random number generator
        private static Random Random = new Random();

        // Compiled XPath expression for matching all nodes and attributes
        private XPathExpression SelectChildrenExpression;

        // List for matched children; used to determine if we matched all required children
        private List<Rule> MatchedRequiredChildren = new List<Rule>(3);

        // Contains the evaluation eventData of this rule while evaluating
        private EvaluatedParentalRule CurrentEvaluationResult;

        // Sync object
        private object LockObject;

        #endregion

        #region .ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="ParentalRule"/> class.
        /// </summary>
        /// <param name="nodeName">Node name.</param>
        protected ParentalRule(string nodeName)
            : this(nodeName, null, NodeValueOptions.IgnoreValue) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ParentalRule"/> class.
        /// </summary>
        /// <param name="nodeName">Node name.</param>
        /// <param name="namespaceUri">Namespace Uri.</param>
        protected ParentalRule(string nodeName, Uri namespaceUri)
            : this(nodeName, namespaceUri, NodeValueOptions.IgnoreValue) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ParentalRule"/> class.
        /// </summary>
        /// <param name="nodeName">Node name.</param>
        /// <param name="options">Node value handling options.</param>
        protected ParentalRule(string nodeName, NodeValueOptions options)
            : this(nodeName, null, options) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ParentalRule"/> class.
        /// </summary>
        /// <param name="nodeName">Node name.</param>
        /// <param name="namespaceUri">Namespace Uri.</param>
        /// <param name="options">Node value handling options.</param>
        protected ParentalRule(string nodeName, Uri namespaceUri, NodeValueOptions options)
            : base(nodeName, namespaceUri, options)
        {
            // Init collections
            this.requiredChildren = new Dictionary<string, Rule>(3);
            this.optionalChildren = new Dictionary<string, Rule>(3);

            // Compile XPath Expressions
            this.SelectChildrenExpression = XPathExpression.Compile("node() | @*");

            // Set default
            this.optionalMatchOption = OptionalEntriesMatchOption.DoNotRequireAnyOptionalEntries;

            // Lock object
            this.LockObject = new object();
        }
        
        #endregion

        #region Properties

        /// <summary>
        /// Gets dictionary containing child rules that MUST be present as child nodes of this rule.
        /// </summary>
        public Dictionary<string, Rule> RequiredChildren
        {
            get
            {
                return this.requiredChildren;
            }
        }

        /// <summary>
        /// Gets dictionary containing child rules that CAN be present as child nodes of this rule.
        /// </summary>
        public Dictionary<string, Rule> OptionalChildren
        {
            get
            {
                return this.optionalChildren;
            }
        }

        /// <summary>
        /// Gets or sets optional entries choice option.
        /// </summary>
        public OptionalEntriesMatchOption OptionalEntriesMatchOption
        {
            get
            {
                return this.optionalMatchOption;
            }
            set
            {
                this.optionalMatchOption = value;
            }
        }

        /// <summary>
        /// Gets or sets maximum count of optional entries to match.
        /// </summary>
        public int MaximumOptionalEntriesToMatch
        {
            get
            {
                return this.maximumOptionalEntriesToMatch;
            }
            set
            {
                // Cannot be negative...
                if (value < 0) throw new ArgumentOutOfRangeException("value");

                // If user set 0 and requires at least one entry at the same time...
                if (value == 0 && this.optionalMatchOption == OptionalEntriesMatchOption.RequireAtLeastOneOptionalEntry)
                    throw new ArgumentException(Resources.ExceptionMsg_MaximumChildsCountToMatchOptionMisunderstood, "value");

                // Set the value
                this.maximumOptionalEntriesToMatch = value;
            }
        }

        #endregion        

        #region Methods

        /// <summary>
        /// Informs about the equality of two rules.
        /// </summary>
        /// <param name="obj">Another rule.</param>
        /// <returns>True if the rules are equal; False if they are different.</returns>
        public override bool Equals(object obj)
        {
            // Cast
            ParentalRule other = obj as ParentalRule;

            // Null?
            if (Object.ReferenceEquals(other, null)) return false;

            // Base equals?
            if (!base.Equals(other)) return false;

            // Check properties
            if (this.optionalMatchOption != other.optionalMatchOption) return false;
            if (this.requiredChildren.Count != other.requiredChildren.Count) return false;
            if (this.optionalChildren.Count != other.optionalChildren.Count) return false;

            // All passed
            return true;
        }

        /// <summary>
        /// Return unique code representing this rule.
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        /// <summary>
        /// == operator overload.
        /// </summary>
        /// <param name="first">First rule.</param>
        /// <param name="second">Second rule.</param>
        /// <returns>True if the rules are equal; False if they are different.</returns>
        public static bool operator ==(ParentalRule first, ParentalRule second)
        {
            return object.Equals(first, second);
        }

        /// <summary>
        /// != operator overload.
        /// </summary>
        /// <param name="first">First rule.</param>
        /// <param name="second">Second rule.</param>
        /// <returns>True if the rules are not equal; False if they are equal.</returns>
        public static bool operator !=(ParentalRule first, ParentalRule second)
        {
            return !object.Equals(first, second);
        }

        /// <summary>
        /// Handles the evaluation of linked rules.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">EventArgs.</param>
        private void PublishEvaluatedRule(object sender, EvaluationEventArgs<EvaluatedRule> e)
        {
            // Did we get the evaluated rule?
            if (this.CurrentEvaluationResult == null) return;

            // Get the rule cookie
            string ruleName = e.EventData.ruleCookie.ToString();

            // Already published?
            if (this.CurrentEvaluationResult.LinkedEvaluatedRules.ContainsKey(ruleName)) return;

            // Keep track of the evaluated linked rules
            this.CurrentEvaluationResult.LinkedEvaluatedRules.Add(ruleName, e.EventData);
        }

        /// <summary>
        /// Links the child rule of this rule to the rule. The child rule will be then accessible directly throught this rule's methods.
        /// </summary>
        /// <param name="rule">Rule to link to this rule.</param>
        public void LinkRule(Rule rule)
        {
            // Check for null...
            if (rule == null) throw new ArgumentNullException("rule");

            // We need properties of an evaluable rule, but we don't the T type parameter of the Evaluable<T> class
            IEvaluableRule evaluableRule = (IEvaluableRule)rule;
            
            // Attach our event handler
            evaluableRule.OnRuleEvaluated += new EventHandler<EvaluationEventArgs<EvaluatedRule>>(PublishEvaluatedRule);
        }

        /// <summary>
        /// Unlinks the child rule from this rule. The child rule will no longer be accessible directly throught this rule's methods.
        /// </summary>
        /// <param name="rule">Rule to unlink from this rule.</param>
        public void UnlinkRule(Rule rule)
        {
            // Check for null...
            if (rule == null) throw new ArgumentNullException("rule");

            // Detach our event handler
            ((IEvaluableRule)rule).OnRuleEvaluated -= new EventHandler<EvaluationEventArgs<EvaluatedRule>>(PublishEvaluatedRule);
        }

        #endregion

        #region Evaluation

        /// <summary>
        /// Evaluates child rules of this rule and passes evaluated rules to provided instance of the <see cref="EvaluatedParentalRule"/> class.
        /// </summary>
        /// <param name="parentRuleNavigator">Navigator pointing to the node represented by this instance.</param>
        /// <param name="parent">Instance of the <see cref="EvaluatedParentalRule"/> class that will accept evaluation results.</param>
        protected void EvaluateChildRules(XPathNavigator parentRuleNavigator, EvaluatedParentalRule evaluatedParentalRule)
        {
            // Check arguments...
            if (evaluatedParentalRule == null || parentRuleNavigator == null) throw new ArgumentNullException();

            lock (this.LockObject)
            {
                try
                {                    
                    this.CurrentEvaluationResult = evaluatedParentalRule;

                    // Resolve value
                    this.ResolveValueOptions(parentRuleNavigator, evaluatedParentalRule);

                    // PublishEvaluatedRule all child nodes
                    XPathNodeIterator childNodesIterator = (XPathNodeIterator)parentRuleNavigator.Evaluate(this.SelectChildrenExpression);

                    // Init counter
                    int optionalChildrenCount = 0;

                    // Go through child nodes...
                    foreach (XPathNavigator childNavigator in childNodesIterator)
                    {
                        // Go through required child rules...
                        foreach (KeyValuePair<string, Rule> childRulePair in this.requiredChildren)
                        {
                            // If this node can be matched by this rule...
                            if (childRulePair.Value.IsMatch(childNavigator))
                            {
                                // Mark this rule as matched...
                                if (!this.MatchedRequiredChildren.Contains(childRulePair.Value))
                                    this.MatchedRequiredChildren.Add(childRulePair.Value);

                                // Evaluate this child rule
                                EvaluateChild(childNavigator, evaluatedParentalRule, childRulePair.Key, childRulePair.Value, true);
                                break;
                            }
                        }

                        // Go through optional child rules...
                        foreach (KeyValuePair<string, Rule> childRulePair in this.optionalChildren)
                        {
                            // If this node can be matched by this rule...
                            if (childRulePair.Value.IsMatch(childNavigator))
                            {
                                // If we matched enought child nodes...
                                if (this.maximumOptionalEntriesToMatch != 0 && optionalChildrenCount >= this.maximumOptionalEntriesToMatch)
                                    break;

                                // Increment counter
                                optionalChildrenCount++;

                                // Evaluate this child rule
                                EvaluateChild(childNavigator, evaluatedParentalRule, childRulePair.Key, childRulePair.Value, false);
                                break;
                            }
                        }
                    }

                    // If there are required children...
                    if (this.requiredChildren.Count > 0)
                    {
                        // Go through required children and check if we have matched each one...
                        foreach (Rule rule in this.requiredChildren.Values)
                            if (!this.MatchedRequiredChildren.Contains(rule))
                                throw new RequiredChildNotFoundException(this, rule, parentRuleNavigator);
                    }

                    // If we matched less optional children than we ought to...
                    if (this.optionalMatchOption == OptionalEntriesMatchOption.RequireAtLeastOneOptionalEntry && this.optionalChildren.Count > 0 && optionalChildrenCount == 0)
                        throw new AtLeastOneOptionalChildExpectedException(this, parentRuleNavigator);
                }
                finally
                {
                    this.MatchedRequiredChildren.Clear();
                    this.CurrentEvaluationResult = null;
                }
            }
        }

        /// <summary>
        /// Evaluates a child rule.
        /// </summary>
        /// <param name="childNavigator">Child navigator.</param>
        /// <param name="parent">Parent of the child rule.</param>
        /// <param name="childRuleKey">Child rule key in the dictionary.</param>
        /// <param name="childRule">Child rule itself.</param>
        /// <param name="required">Insert this child rule into the dictionary for required children.</param>
        private static void EvaluateChild(XPathNavigator childNavigator, EvaluatedParentalRule parent, string childRuleKey, Rule childRule, bool required)
        {
            // Declare eventData
            EvaluatedRule evaluatedChildRule = null;

            // Cast the child rule to IEvaluableRule interface providing methods common for evaluable rules
            IEvaluableRule evaluableChildRule = (IEvaluableRule)childRule;

            // Evaluate child rule
            evaluatedChildRule = evaluableChildRule.Evaluate(childNavigator);

            // Check the eventData
            if (evaluatedChildRule == null) 
                throw new InvalidOperationException(String.Format(Resources.ExceptionMsg_Formatable1_InvalidRuleImpl, childRule));

            // Raise OnRuleEvaluated event            
            evaluableChildRule.RaiseRuleEvaluatedEvent(evaluatedChildRule);
                        
            // Init key
            string uniqueKey = childRuleKey;

            // Choose target dictionary
            Dictionary<string, EvaluatedRule> target = required ? parent.RequiredChildren : parent.OptionalChildren;

            // Is key unique? No, so build a one...
            if (target.ContainsKey(uniqueKey))
                uniqueKey = String.Format("{0}{1}{2}", childRuleKey, EvaluatedRuleNumberSeparator, Random.Next().ToString());

            // Insert evaluated rule into the dictionary
            target.Add(uniqueKey, evaluatedChildRule);
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Tomas Deml

Czech Republic Czech Republic
I'm a student of the Low-voltage Electrical Engineering specialized on Computing from the Czech republic.
 
I'm a C# kind of guy, fan of .NET.
 
I've formed a programming group called 'Comnicate!'. Currently the only member of the group is myself. Wink | ;-)

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 18 Oct 2005
Article Copyright 2005 by Tomas Deml
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid