File: System\Data\Services\KeyInstance.cs
Project: ndp\fx\src\DataWeb\Server\System.Data.Services.csproj (System.Data.Services)
//---------------------------------------------------------------------
// <copyright file="KeyInstance.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides a class used to represent a key value for a resource.
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services
{
    using System.Collections.Generic;
    using System.Data.Services.Parsing;
    using System.Data.Services.Providers;
    using System.Diagnostics;
 
    /// <summary>Provides a class used to represent a key for a resource.</summary>
    /// <remarks>
    /// Internally, every key instance has a collection of values. These values
    /// can be named or positional, depending on how they were specified
    /// if parsed from a URI.
    /// </remarks>
    internal class KeyInstance
    {
        /// <summary>Empty key singleton.</summary>
        private static readonly KeyInstance Empty = new KeyInstance();
 
        /// <summary>Named values.</summary>
        private readonly Dictionary<string, object> namedValues;
 
        /// <summary>Positional values.</summary>
        private readonly List<object> positionalValues;
 
        /// <summary>Initializes a new empty <see cref="KeyInstance"/> instance.</summary>
        private KeyInstance()
        {
        }
 
        /// <summary>Initializes a new <see cref="KeyInstance"/> instance.</summary>
        /// <param name='namedValues'>Named values.</param>
        /// <param name='positionalValues'>Positional values for this instance.</param>
        /// <remarks>
        /// One of namedValues or positionalValues should be non-null, but not both.
        /// </remarks>
        private KeyInstance(Dictionary<string, object> namedValues, List<object> positionalValues)
        {
            Debug.Assert(
                (namedValues == null) != (positionalValues == null),
                "namedValues == null != positionalValues == null -- one or the other should be assigned, but not both");
            this.namedValues = namedValues;
            this.positionalValues = positionalValues;
        }
 
        /// <summary>Whether the values have a name.</summary>
        internal bool AreValuesNamed
        {
            get { return this.namedValues != null; }
        }
 
        /// <summary>Checks whether this key has any values.</summary>
        internal bool IsEmpty
        {
            get { return this == Empty; }
        }
 
        /// <summary>Returns a dictionary of named values when they AreValuesNamed is true.</summary>
        internal IDictionary<string, object> NamedValues
        {
            get { return this.namedValues; }
        }
 
        /// <summary>Returns a list of values when they AreValuesNamed is false.</summary>
        internal IList<object> PositionalValues
        {
            get { return this.positionalValues; }
        }
 
        /// <summary>Number of values in the key.</summary>
        internal int ValueCount
        {
            get
            {
                if (this == Empty)
                {
                    return 0;
                }
                else if (this.namedValues != null)
                {
                    return this.namedValues.Count;
                }
                else
                {
                    Debug.Assert(this.positionalValues != null, "this.positionalValues != null");
                    return this.positionalValues.Count;
                }
            }
        }
 
        /// <summary>Attempts to parse key values from the specified text.</summary>
        /// <param name='text'>Text to parse (not null).</param>
        /// <param name='instance'>After invocation, the parsed key instance.</param>
        /// <returns>
        /// true if the key instance was parsed; false if there was a 
        /// syntactic error.
        /// </returns>
        /// <remarks>
        /// The returned instance contains only string values. To get typed values, a call to
        /// <see cref="TryConvertValues"/> is necessary.
        /// </remarks>
        internal static bool TryParseKeysFromUri(string text, out KeyInstance instance)
        {
            return TryParseFromUri(text, true /*allowNamedValues*/, false /*allowNull*/, out instance);
        }
 
        /// <summary>Attempts to parse nullable values (only positional values, no name-value pairs) from the specified text.</summary>
        /// <param name='text'>Text to parse (not null).</param>
        /// <param name='instance'>After invocation, the parsed key instance.</param>
        /// <returns>
        /// true if the given values were parsed; false if there was a 
        /// syntactic error.
        /// </returns>
        /// <remarks>
        /// The returned instance contains only string values. To get typed values, a call to
        /// <see cref="TryConvertValues"/> is necessary.
        /// </remarks>
        internal static bool TryParseNullableTokens(string text, out KeyInstance instance)
        {
            return TryParseFromUri(text, false /*allowNamedValues*/, true /*allowNull*/, out instance);
        }
 
        /// <summary>Tries to convert values to the keys of the specified type.</summary>
        /// <param name="type">Type with key information for conversion.</param>
        /// <returns>true if all values were converted; false otherwise.</returns>
        internal bool TryConvertValues(ResourceType type)
        {
            Debug.Assert(type != null, "type != null");
            Debug.Assert(!this.IsEmpty, "!this.IsEmpty -- caller should check");
            Debug.Assert(type.KeyProperties.Count == this.ValueCount, "type.KeyProperties.Count == this.ValueCount -- will change with containment");
            if (this.namedValues != null)
            {
                for (int i = 0; i < type.KeyProperties.Count; i++)
                {
                    ResourceProperty property = type.KeyProperties[i];
                    object unconvertedValue;
                    if (!this.namedValues.TryGetValue(property.Name, out unconvertedValue))
                    {
                        return false;
                    }
 
                    string valueText = (string)unconvertedValue;
                    object convertedValue;
                    if (!WebConvert.TryKeyStringToPrimitive(valueText, property.Type, out convertedValue))
                    {
                        return false;
                    }
 
                    this.namedValues[property.Name] = convertedValue;
                }
            }
            else
            {
                Debug.Assert(this.positionalValues != null, "positionalValues != null -- otherwise this is Empty");
                for (int i = 0; i < type.KeyProperties.Count; i++)
                {
                    string valueText = (string)this.positionalValues[i];
                    object convertedValue;
                    if (!WebConvert.TryKeyStringToPrimitive(valueText, type.KeyProperties[i].Type, out convertedValue))
                    {
                        return false;
                    }
 
                    this.positionalValues[i] = convertedValue;
                }
            }
 
            return true;
        }
 
        /// <summary>Attempts to parse key values from the specified text.</summary>
        /// <param name='text'>Text to parse (not null).</param>
        /// <param name="allowNamedValues">Set to true if the parser should accept named values
        ///   so syntax like Name='value'. If this is false, the parsing will fail on such constructs.</param>
        /// <param name="allowNull">Set to true if the parser should accept null values.
        ///   If set to false, the parser will fail on null values.</param>
        /// <param name='instance'>After invocation, the parsed key instance.</param>
        /// <returns>
        /// true if the key instance was parsed; false if there was a 
        /// syntactic error.
        /// </returns>
        /// <remarks>
        /// The returned instance contains only string values. To get typed values, a call to
        /// <see cref="TryConvertValues"/> is necessary.
        /// </remarks>
        private static bool TryParseFromUri(string text, bool allowNamedValues, bool allowNull, out KeyInstance instance)
        {
            Debug.Assert(text != null, "text != null");
 
            Dictionary<string, object> namedValues = null;
            List<object> positionalValues = null;
 
            ExpressionLexer lexer = new ExpressionLexer(text);
            Token currentToken = lexer.CurrentToken;
            if (currentToken.Id == TokenId.End)
            {
                instance = Empty;
                return true;
            }
 
            instance = null;
            do
            {
                if (currentToken.Id == TokenId.Identifier && allowNamedValues)
                {
                    // Name-value pair.
                    if (positionalValues != null)
                    {
                        // We cannot mix named and non-named values.
                        return false;
                    }
 
                    string identifier = lexer.CurrentToken.GetIdentifier();
                    lexer.NextToken();
                    if (lexer.CurrentToken.Id != TokenId.Equal)
                    {
                        return false;
                    }
 
                    lexer.NextToken();
                    if (!lexer.CurrentToken.IsKeyValueToken)
                    {
                        return false;
                    }
 
                    string namedValue = lexer.CurrentToken.Text;
                    WebUtil.CreateIfNull(ref namedValues);
                    if (namedValues.ContainsKey(identifier))
                    {
                        // Duplicate name.
                        return false;
                    }
 
                    namedValues.Add(identifier, namedValue);
                }
                else if (currentToken.IsKeyValueToken || (allowNull && currentToken.Id == TokenId.NullLiteral))
                {
                    // Positional value.
                    if (namedValues != null)
                    {
                        // We cannot mix named and non-named values.
                        return false;
                    }
 
                    WebUtil.CreateIfNull(ref positionalValues);
                    positionalValues.Add(lexer.CurrentToken.Text);
                }
                else
                {
                    return false;
                }
 
                // Read the next token. We should be at the end, or find
                // we have a comma followed by something.
                lexer.NextToken();
                currentToken = lexer.CurrentToken;
                if (currentToken.Id == TokenId.Comma)
                {
                    lexer.NextToken();
                    currentToken = lexer.CurrentToken;
                    if (currentToken.Id == TokenId.End)
                    {
                        // Trailing comma.
                        return false;
                    }
                }
            }
            while (currentToken.Id != TokenId.End);
 
            instance = new KeyInstance(namedValues, positionalValues);
            return true;
        }
    }
}