Exercise 6: Solution

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Plato.Core;

namespace Plato.Language.Parser
{
    /// <summary>
    /// Parses transformations.
    /// </summary>
    public class TransformationParser
    {
        Brain brain;

        public TransformationParser(Brain brain)
        {
            this.brain = brain;
        }

        /// <summary>
        /// Parse the lines of input into transformations.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>A list of parsed transformations.</returns>
        public IList<Transformation> Parse(string str)
        {
            IList<Transformation> transformations = new List<Transformation>();

            string[] lines = str.Split(new char[] { '\n', '\r' },
                                       StringSplitOptions.RemoveEmptyEntries);

            // Process each line of input
            int lineNum = 0;
            foreach (string line in lines)
            {
                ++lineNum;
                line.Trim();

                if (String.IsNullOrEmpty(line))
                {
                    continue;
                }

                try
                {
                    string input;
                    string output;

                    if (line.Contains("->"))
                    {
                        string[] parts = line.Split(new string[] { "->" },
                                                    StringSplitOptions.None);
                        if (parts.Length != 2)
                        {
                            throw new TooManyArrowsException(
                                "Expecting a single '->'");
                        }
                        else
                        {
                            input = parts[0].Trim();
                            output = parts[1].Trim();

                            TransInput transInput = ParseTransInput(input);
                            ITransOutput transOutput = ParseTransOutput(output);

                            Transformation t =
                                new Transformation(transInput, transOutput);
                            transformations.Add(t);
                        }
                    }
                    else
                    {
                        throw new MissingArrowException("Missing '->'");
                    }
                }
                catch (AlterableException e)
                {
                    e.AlterMessage(
                        String.Format("Line {0}: {1}",
                                      lineNum, e.Message));
                    throw e;
                }
                catch (Exception e)
                {
                    throw new Exception(
                        String.Format("Line {0}: {1}",
                                      lineNum, e.Message));
                }
            }

            return transformations;
        }

        /// <summary>
        /// Parse a transformation input specification.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>A parsed transformation input spec.</returns>
        protected TransInput ParseTransInput(string str)
        {
            IList<IInputToken> tokenList = new List<IInputToken>();

            string[] tokens = str.Split(new char[] { ' ' },
                                        StringSplitOptions.RemoveEmptyEntries);

            if (tokens.Length == 0)
            {
                throw new EmptyInputException("The input specification is empty.");
            }

            // Process each token
            foreach (string token in tokens)
            {
                // An entity type token?
                if (token.StartsWith("{"))
                {
                    tokenList.Add(ParseEntityTypeToken(token));
                }
                else
                {
                    // A literal token
                    tokenList.Add(new LiteralToken(token));
                }
            }

            return new TransInput(tokenList);
        }

        /// <summary>
        /// Parse a transformation output specification.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>A parsed transformation output spec.</returns>
        protected ITransOutput ParseTransOutput(string str)
        {
            if (str.Contains("="))
            {
                return ParseTransOutput_Assignment(str);
            }
            else
            {
                return ParseTransOutput_Value(str);
            }
        }

        /// <summary>
        /// Parse a transformation output specification that is
        /// an assignment.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>A parsed transformation output spec.</returns>
        protected TransOutput_Assignment ParseTransOutput_Assignment(string str)
        {
            PropertyToken property;
            VariableToken value;

            string[] parts = str.Split(new string[] { "=" },
                                       StringSplitOptions.None);
            if (parts.Length != 2)
            {
                throw new TooManyEqualSignsException(
                    "Expecting a single '=' in the output specification.");
            }
            else
            {
                string propertyToken = parts[0].Trim();
                string valueToken = parts[1].Trim();

                property = ParseProperty(propertyToken);
                value = ParseVariableToken(valueToken);
            }

            return new TransOutput_Assignment(property, value);
        }

        /// <summary>
        /// Parse a transformation output specification that is
        /// a value.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>A parsed transformation output spec.</returns>
        protected virtual TransOutput_Value ParseTransOutput_Value(string str)
        {
            return new TransOutput_Value(ParseProperty(str));
        }

        /// <summary>
        /// Parse an entity type token.
        /// </summary>
        /// <param name="token">The text input.</param>
        /// <returns>The parsed token.</returns>
        protected EntityTypeToken ParseEntityTypeToken(string token)
        {
            if (!token.EndsWith("}"))
            {
                throw new MissingClosingBraceException(
                    String.Format("The entity type '{0}' is missing " +
                                  "its closing brace.", token));
            }

            // Remove the curly braces
            string entityId = token.Substring(1, token.Length - 2);

            IEntity entity = brain.Get(entityId);

            if (entity == null)
            {
                throw new EntityNotFoundException(
                    String.Format("The entity type '{0}' could " +
                                  "not be found.", entityId));
            }

            return new EntityTypeToken(entity);
        }

        /// <summary>
        /// Parse a variable token.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>The parsed token.</returns>
        protected VariableToken ParseVariableToken(string token)
        {
            // Variable token
            try
            {
                string variableNumber = token.Substring(1, token.Length - 1);
                return new VariableToken(Convert.ToUInt32(variableNumber));
            }
            catch
            {
                throw new InvalidVariableTokenException(
                    String.Format("The variable token '{0}' is invalid.",
                                  token));
            }
        }

        /// <summary>
        /// Parse a property.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>A parsed property.</returns>
        protected virtual PropertyToken ParseProperty(string str)
        {
            IList<IPropertyPathToken> path = new List<IPropertyPathToken>();

            // Note: It is conceivable that a fragment could contain
            // a period and so this isn't quite right.
            string[] tokens = str.Split('.');

            foreach (string token in tokens)
            {
                if (token.StartsWith("(("))
                {
                    // Fragment token
                    if (token.EndsWith("))"))
                    {
                        string fragmentToken = token.Substring(2, token.Length - 4);
                        FragmentToken fragment = new FragmentToken(
                            ParseOutputFragment(fragmentToken));
                        path.Add(fragment);
                    }
                    else
                    {
                        throw new InvalidTokenException(
                            String.Format("Invalid: {0}.", token));
                    }
                }
                else if (token.StartsWith("$"))
                {
                    path.Add(ParseVariableToken(token));
                }
                else if (token.Length == 0)
                {
                    throw new InvalidPropertyException(
                        String.Format("Invalid property: {0}", str));
                }
                else
                {
                    // Entity token
                    IEntity entity = brain.Get(token);
                    if (entity == null)
                    {
                        throw new EntityNotFoundException(
                            String.Format("The entity '{0}' could " +
                                          "not be found.", token));
                    }
                    path.Add(new EntityToken(entity));
                }
            }

            return new PropertyToken(path);
        }

        /// <summary>
        /// Parse a fragment.
        /// </summary>
        /// <param name="str">The text input.</param>
        /// <returns>The parsed fragment.</returns>
        protected Fragment ParseOutputFragment(string str)
        {
            IList<IToken> tokenList = new List<IToken>();

            string[] tokens = str.Split(new char[] { ' ' },
                                        StringSplitOptions.RemoveEmptyEntries);

            if (tokens.Length == 0)
            {
                throw new EmptyFragmentException("The fragment is empty.");
            }

            // Process each token
            foreach (string token in tokens)
            {
                if (token.StartsWith("$"))
                {
                    // Variable token
                    tokenList.Add(ParseVariableToken(token));
                }
                else
                {
                    // Literal token
                    tokenList.Add(new LiteralToken(token));
                }
            }

            return new Fragment(tokenList);
        }
    }

    /// <summary>
    /// An exception whos message can be altered.
    /// </summary>
    public class AlterableException : Exception
    {
        private string message;

        public AlterableException(string message)
            : base(message)
        {
        }

        public override string Message
        {
            get
            {
                if (String.IsNullOrEmpty(message))
                {
                    // If the message hasn't been altered, return
                    // the message of the original exception.
                    return base.Message;
                }
                else
                {
                    // If the message has been altered, return
                    // the altered message;
                    return message;
                }
            }
        }

        public void AlterMessage(string message)
        {
            this.message = message;
        }
    }

    public class MissingArrowException : AlterableException
    {
        public MissingArrowException(string message)
            : base(message)
        {
        }
    }

    public class TooManyArrowsException : AlterableException
    {
        public TooManyArrowsException(string message)
            : base(message)
        {
        }
    }

    public class EmptyInputException : AlterableException
    {
        public EmptyInputException(string message)
            : base(message)
        {
        }
    }

    public class MissingClosingBraceException : AlterableException
    {
        public MissingClosingBraceException(string message)
            : base(message)
        {
        }
    }

    public class EntityNotFoundException : AlterableException
    {
        public EntityNotFoundException(string message)
            : base(message)
        {
        }
    }

    public class TooManyEqualSignsException : AlterableException
    {
        public TooManyEqualSignsException(string message)
            : base(message)
        {
        }
    }

    public class InvalidTokenException : AlterableException
    {
        public InvalidTokenException(string message)
            : base(message)
        {
        }
    }

    public class EmptyFragmentException : AlterableException
    {
        public EmptyFragmentException(string message)
            : base(message)
        {
        }
    }

    ...