File: System\Data\Services\Client\ALinq\UriWriter.cs
Project: ndp\fx\src\DataWeb\Client\System.Data.Services.Client.csproj (System.Data.Services.Client)
//---------------------------------------------------------------------
// <copyright file="UriWriter.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Translates resource bound expression trees into URIs.
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services.Client
{
    #region Namespaces.
 
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Text;
 
    #endregion Namespaces.
 
    /// <summary>
    /// Translates resource bound expression trees into URIs.
    /// </summary>
    internal class UriWriter : DataServiceALinqExpressionVisitor
    {
        /// <summary>Data context used to generate type names for types.</summary>
        private readonly DataServiceContext context;
 
        /// <summary>stringbuilder for constructed URI</summary>
        private readonly StringBuilder uriBuilder;
        
        /// <summary>the data service version for the uri</summary>
        private Version uriVersion;
 
        /// <summary>the leaf resourceset for the URI being written</summary>
        private ResourceSetExpression leafResourceSet;
        
        /// <summary>
        /// Private constructor for creating UriWriter
        /// </summary>
        /// <param name='context'>Data context used to generate type names for types.</param>
        private UriWriter(DataServiceContext context)
        {
            Debug.Assert(context != null, "context != null");
            this.context = context;
            this.uriBuilder = new StringBuilder();
            this.uriVersion = Util.DataServiceVersion1;
        }
 
        /// <summary>
        /// Translates resource bound expression tree to a URI.
        /// </summary>
        /// <param name='context'>Data context used to generate type names for types.</param>
        /// <param name="addTrailingParens">flag to indicate whether generated URI should include () if leaf is ResourceSet</param>
        /// <param name="e">The expression to translate</param>
        /// <param name="uri">uri</param>
        /// <param name="version">version for query</param>    
        internal static void Translate(DataServiceContext context, bool addTrailingParens, Expression e, out Uri uri, out Version version)
        {
            var writer = new UriWriter(context);
            writer.leafResourceSet = addTrailingParens ? (e as ResourceSetExpression) : null;
            writer.Visit(e);
            uri = Util.CreateUri(context.BaseUriWithSlash, Util.CreateUri(writer.uriBuilder.ToString(), UriKind.Relative));
            version = writer.uriVersion;
        }
 
        /// <summary>
        /// MethodCallExpression visit method
        /// </summary>
        /// <param name="m">The MethodCallExpression expression to visit</param>
        /// <returns>The visited MethodCallExpression expression </returns>
        internal override Expression VisitMethodCall(MethodCallExpression m)
        {
            throw Error.MethodNotSupported(m);
        }
 
        /// <summary>
        /// UnaryExpression visit method
        /// </summary>
        /// <param name="u">The UnaryExpression expression to visit</param>
        /// <returns>The visited UnaryExpression expression </returns>
        internal override Expression VisitUnary(UnaryExpression u)
        {
            throw new NotSupportedException(Strings.ALinq_UnaryNotSupported(u.NodeType.ToString()));
        }
 
        /// <summary>
        /// BinaryExpression visit method
        /// </summary>
        /// <param name="b">The BinaryExpression expression to visit</param>
        /// <returns>The visited BinaryExpression expression </returns>
        internal override Expression VisitBinary(BinaryExpression b)
        {
            throw new NotSupportedException(Strings.ALinq_BinaryNotSupported(b.NodeType.ToString()));
        }
 
        /// <summary>
        /// ConstantExpression visit method
        /// </summary>
        /// <param name="c">The ConstantExpression expression to visit</param>
        /// <returns>The visited ConstantExpression expression </returns>
        internal override Expression VisitConstant(ConstantExpression c)
        {
            throw new NotSupportedException(Strings.ALinq_ConstantNotSupported(c.Value));
        }
 
        /// <summary>
        /// TypeBinaryExpression visit method
        /// </summary>
        /// <param name="b">The TypeBinaryExpression expression to visit</param>
        /// <returns>The visited TypeBinaryExpression expression </returns>
        internal override Expression VisitTypeIs(TypeBinaryExpression b)
        {
            throw new NotSupportedException(Strings.ALinq_TypeBinaryNotSupported);
        }
 
        /// <summary>
        /// ConditionalExpression visit method
        /// </summary>
        /// <param name="c">The ConditionalExpression expression to visit</param>
        /// <returns>The visited ConditionalExpression expression </returns>
        internal override Expression VisitConditional(ConditionalExpression c)
        {
            throw new NotSupportedException(Strings.ALinq_ConditionalNotSupported);
        }
 
        /// <summary>
        /// ParameterExpression visit method
        /// </summary>
        /// <param name="p">The ParameterExpression expression to visit</param>
        /// <returns>The visited ParameterExpression expression </returns>
        internal override Expression VisitParameter(ParameterExpression p)
        {
            throw new NotSupportedException(Strings.ALinq_ParameterNotSupported);
        }
 
        /// <summary>
        /// MemberExpression visit method
        /// </summary>
        /// <param name="m">The MemberExpression expression to visit</param>
        /// <returns>The visited MemberExpression expression </returns>
        internal override Expression VisitMemberAccess(MemberExpression m)
        {
            throw new NotSupportedException(Strings.ALinq_MemberAccessNotSupported(m.Member.Name));
        }
 
        /// <summary>
        /// LambdaExpression visit method
        /// </summary>
        /// <param name="lambda">The LambdaExpression to visit</param>
        /// <returns>The visited LambdaExpression</returns>
        internal override Expression VisitLambda(LambdaExpression lambda)
        {
            throw new NotSupportedException(Strings.ALinq_LambdaNotSupported);
        }
 
        /// <summary>
        /// NewExpression visit method
        /// </summary>
        /// <param name="nex">The NewExpression to visit</param>
        /// <returns>The visited NewExpression</returns>
        internal override NewExpression VisitNew(NewExpression nex)
        {
            throw new NotSupportedException(Strings.ALinq_NewNotSupported);
        }
 
        /// <summary>
        /// MemberInitExpression visit method
        /// </summary>
        /// <param name="init">The MemberInitExpression to visit</param>
        /// <returns>The visited MemberInitExpression</returns>
        internal override Expression VisitMemberInit(MemberInitExpression init)
        {
            throw new NotSupportedException(Strings.ALinq_MemberInitNotSupported);
        }
 
        /// <summary>
        /// ListInitExpression visit method
        /// </summary>
        /// <param name="init">The ListInitExpression to visit</param>
        /// <returns>The visited ListInitExpression</returns>
        internal override Expression VisitListInit(ListInitExpression init)
        {
            throw new NotSupportedException(Strings.ALinq_ListInitNotSupported);
        }
 
        /// <summary>
        /// NewArrayExpression visit method
        /// </summary>
        /// <param name="na">The NewArrayExpression to visit</param>
        /// <returns>The visited NewArrayExpression</returns>
        internal override Expression VisitNewArray(NewArrayExpression na)
        {
            throw new NotSupportedException(Strings.ALinq_NewArrayNotSupported);
        }
 
        /// <summary>
        /// InvocationExpression visit method
        /// </summary>
        /// <param name="iv">The InvocationExpression to visit</param>
        /// <returns>The visited InvocationExpression</returns>
        internal override Expression VisitInvocation(InvocationExpression iv)
        {
            throw new NotSupportedException(Strings.ALinq_InvocationNotSupported);
        }
 
        /// <summary>
        /// NavigationPropertySingletonExpression visit method.
        /// </summary>
        /// <param name="npse">NavigationPropertySingletonExpression expression to visit</param>
        /// <returns>Visited NavigationPropertySingletonExpression expression</returns>
        internal override Expression VisitNavigationPropertySingletonExpression(NavigationPropertySingletonExpression npse)
        {
            this.Visit(npse.Source);
            this.uriBuilder.Append(UriHelper.FORWARDSLASH).Append(this.ExpressionToString(npse.MemberExpression));
            this.VisitQueryOptions(npse);
            return npse;
        }
 
        /// <summary>
        /// ResourceSetExpression visit method.
        /// </summary>
        /// <param name="rse">ResourceSetExpression expression to visit</param>
        /// <returns>Visited ResourceSetExpression expression</returns>
        internal override Expression VisitResourceSetExpression(ResourceSetExpression rse)
        {
            if ((ResourceExpressionType)rse.NodeType == ResourceExpressionType.ResourceNavigationProperty)
            {
                this.Visit(rse.Source);
                this.uriBuilder.Append(UriHelper.FORWARDSLASH).Append(this.ExpressionToString(rse.MemberExpression));
            }
            else
            {
                this.uriBuilder.Append(UriHelper.FORWARDSLASH).Append((string)((ConstantExpression)rse.MemberExpression).Value);
            }
 
            if (rse.KeyPredicate != null)
            {
                this.uriBuilder.Append(UriHelper.LEFTPAREN);
                if (rse.KeyPredicate.Count == 1)
                {
                    this.uriBuilder.Append(this.ExpressionToString(rse.KeyPredicate.Values.First()));
                }
                else
                {
                    bool addComma = false;
                    foreach (var kvp in rse.KeyPredicate)
                    {
                        if (addComma)
                        {
                            this.uriBuilder.Append(UriHelper.COMMA);
                        }
 
                        this.uriBuilder.Append(kvp.Key.Name);
                        this.uriBuilder.Append(UriHelper.EQUALSSIGN);
                        this.uriBuilder.Append(this.ExpressionToString(kvp.Value));
                        addComma = true;
                    }
                }
 
                this.uriBuilder.Append(UriHelper.RIGHTPAREN);
            }
            else if (rse == this.leafResourceSet)
            {
                // if resourceset is on the leaf, append ()
                this.uriBuilder.Append(UriHelper.LEFTPAREN);
                this.uriBuilder.Append(UriHelper.RIGHTPAREN);
            }
 
            if (rse.CountOption == CountOption.ValueOnly)
            {
                // append $count segment: /$count
                this.uriBuilder.Append(UriHelper.FORWARDSLASH).Append(UriHelper.DOLLARSIGN).Append(UriHelper.COUNT);
                this.EnsureMinimumVersion(2, 0);
            }
 
            this.VisitQueryOptions(rse);
            return rse;
        }
 
        /// <summary>
        /// Visit Query options for Resource
        /// </summary>
        /// <param name="re">Resource Expression with query options</param>
        internal void VisitQueryOptions(ResourceExpression re)
        {
            bool needAmpersand = false;
 
            if (re.HasQueryOptions)
            {
                this.uriBuilder.Append(UriHelper.QUESTIONMARK);
 
                ResourceSetExpression rse = re as ResourceSetExpression;
                if (rse != null)
                {
                    IEnumerator options = rse.SequenceQueryOptions.GetEnumerator();
                    while (options.MoveNext())
                    {
                        if (needAmpersand)
                        {
                            this.uriBuilder.Append(UriHelper.AMPERSAND);
                        }
 
                        Expression e = ((Expression)options.Current);
                        ResourceExpressionType et = (ResourceExpressionType)e.NodeType;
                        switch (et)
                        {
                            case ResourceExpressionType.SkipQueryOption:
                                this.VisitQueryOptionExpression((SkipQueryOptionExpression)e);
                                break;
                            case ResourceExpressionType.TakeQueryOption:
                                this.VisitQueryOptionExpression((TakeQueryOptionExpression)e);
                                break;
                            case ResourceExpressionType.OrderByQueryOption:
                                this.VisitQueryOptionExpression((OrderByQueryOptionExpression)e);
                                break;
                            case ResourceExpressionType.FilterQueryOption:
                                this.VisitQueryOptionExpression((FilterQueryOptionExpression)e);
                                break;
                            default:
                                Debug.Assert(false, "Unexpected expression type " + (int)et);
                                break;
                        }
 
                        needAmpersand = true;
                    }
                }
 
                if (re.ExpandPaths.Count > 0)
                {
                    if (needAmpersand)
                    {
                        this.uriBuilder.Append(UriHelper.AMPERSAND);
                    }
 
                    this.VisitExpandOptions(re.ExpandPaths);
                    needAmpersand = true;
                }
 
                if (re.Projection != null && re.Projection.Paths.Count > 0)
                {
                    if (needAmpersand)
                    {
                        this.uriBuilder.Append(UriHelper.AMPERSAND);
                    }
 
                    this.VisitProjectionPaths(re.Projection.Paths);
                    needAmpersand = true;
                }
 
                if (re.CountOption == CountOption.InlineAll)
                {
                    if (needAmpersand)
                    {
                        this.uriBuilder.Append(UriHelper.AMPERSAND);
                    }
 
                    this.VisitCountOptions();
                    needAmpersand = true;
                }
 
                if (re.CustomQueryOptions.Count > 0)
                {
                    if (needAmpersand)
                    {
                        this.uriBuilder.Append(UriHelper.AMPERSAND);
                    }
 
                    this.VisitCustomQueryOptions(re.CustomQueryOptions);
                    needAmpersand = true;
                }
            }
        }
 
        /// <summary>
        /// SkipQueryOptionExpression visit method.
        /// </summary>
        /// <param name="sqoe">SkipQueryOptionExpression expression to visit</param>
        internal void VisitQueryOptionExpression(SkipQueryOptionExpression sqoe)
        {
            this.uriBuilder.Append(UriHelper.DOLLARSIGN);
            this.uriBuilder.Append(UriHelper.OPTIONSKIP);
            this.uriBuilder.Append(UriHelper.EQUALSSIGN);
            this.uriBuilder.Append(this.ExpressionToString(sqoe.SkipAmount));
        }
 
        /// <summary>
        /// TakeQueryOptionExpression visit method.
        /// </summary>
        /// <param name="tqoe">TakeQueryOptionExpression expression to visit</param>
        internal void VisitQueryOptionExpression(TakeQueryOptionExpression tqoe)
        {
            this.uriBuilder.Append(UriHelper.DOLLARSIGN);
            this.uriBuilder.Append(UriHelper.OPTIONTOP);
            this.uriBuilder.Append(UriHelper.EQUALSSIGN);
            this.uriBuilder.Append(this.ExpressionToString(tqoe.TakeAmount));
        }
 
        /// <summary>
        /// FilterQueryOptionExpression visit method.
        /// </summary>
        /// <param name="fqoe">FilterQueryOptionExpression expression to visit</param>
        internal void VisitQueryOptionExpression(FilterQueryOptionExpression fqoe)
        {
            this.uriBuilder.Append(UriHelper.DOLLARSIGN);
            this.uriBuilder.Append(UriHelper.OPTIONFILTER);
            this.uriBuilder.Append(UriHelper.EQUALSSIGN);
            this.uriBuilder.Append(this.ExpressionToString(fqoe.Predicate));
        }
 
        /// <summary>
        /// OrderByQueryOptionExpression visit method.
        /// </summary>
        /// <param name="oboe">OrderByQueryOptionExpression expression to visit</param>
        internal void VisitQueryOptionExpression(OrderByQueryOptionExpression oboe)
        {
            this.uriBuilder.Append(UriHelper.DOLLARSIGN);
            this.uriBuilder.Append(UriHelper.OPTIONORDERBY);
            this.uriBuilder.Append(UriHelper.EQUALSSIGN);
 
            int ii = 0;
            while (true)
            {
                var selector = oboe.Selectors[ii];
 
                this.uriBuilder.Append(this.ExpressionToString(selector.Expression));
                if (selector.Descending)
                {
                    this.uriBuilder.Append(UriHelper.SPACE);
                    this.uriBuilder.Append(UriHelper.OPTIONDESC);
                }
 
                if (++ii == oboe.Selectors.Count)
                {
                    break;
                }
 
                this.uriBuilder.Append(UriHelper.COMMA);
            }
        }
 
        /// <summary>
        /// VisitExpandOptions visit method.
        /// </summary>
        /// <param name="paths">Expand Paths</param>
        internal void VisitExpandOptions(List<string> paths)
        {
            this.uriBuilder.Append(UriHelper.DOLLARSIGN);
            this.uriBuilder.Append(UriHelper.OPTIONEXPAND);
            this.uriBuilder.Append(UriHelper.EQUALSSIGN);
 
            int ii = 0;
            while (true)
            {
                this.uriBuilder.Append(paths[ii]);
 
                if (++ii == paths.Count)
                {
                    break;
                }
 
                this.uriBuilder.Append(UriHelper.COMMA);
            }
        }
 
        /// <summary>
        /// ProjectionPaths visit method.
        /// </summary>
        /// <param name="paths">Projection Paths</param>
        internal void VisitProjectionPaths(List<string> paths)
        {
            this.uriBuilder.Append(UriHelper.DOLLARSIGN);
            this.uriBuilder.Append(UriHelper.OPTIONSELECT);
            this.uriBuilder.Append(UriHelper.EQUALSSIGN);
 
            int ii = 0;
            while (true)
            {
                string path = paths[ii];
 
                this.uriBuilder.Append(path);
 
                if (++ii == paths.Count)
                {
                    break;
                }
 
                this.uriBuilder.Append(UriHelper.COMMA);
            }
 
            this.EnsureMinimumVersion(2, 0);
        }
 
        /// <summary>
        /// VisitCountOptions visit method.
        /// </summary>
        internal void VisitCountOptions()
        {
            this.uriBuilder.Append(UriHelper.DOLLARSIGN);
            this.uriBuilder.Append(UriHelper.OPTIONCOUNT);
            this.uriBuilder.Append(UriHelper.EQUALSSIGN);
            this.uriBuilder.Append(UriHelper.COUNTALL);
            this.EnsureMinimumVersion(2, 0);
        }
 
        /// <summary>
        /// VisitCustomQueryOptions visit method.
        /// </summary>
        /// <param name="options">Custom query options</param>
        internal void VisitCustomQueryOptions(Dictionary<ConstantExpression, ConstantExpression> options)
        {
            List<ConstantExpression> keys = options.Keys.ToList();
            List<ConstantExpression> values = options.Values.ToList();
 
            int ii = 0;
            while (true)
            {
                this.uriBuilder.Append(keys[ii].Value);
                this.uriBuilder.Append(UriHelper.EQUALSSIGN);
                this.uriBuilder.Append(values[ii].Value);
 
                if (keys[ii].Value.ToString().Equals(UriHelper.DOLLARSIGN + UriHelper.OPTIONCOUNT, StringComparison.OrdinalIgnoreCase))
                {
                    this.EnsureMinimumVersion(2, 0);
                }
 
                if (++ii == keys.Count)
                {
                    break;
                }
 
                this.uriBuilder.Append(UriHelper.AMPERSAND);
            }
        }
 
        /// <summary>Serializes an expression to a string.</summary>
        /// <param name="expression">Expression to serialize</param>
        /// <returns>The serialized expression.</returns>
        private string ExpressionToString(Expression expression)
        {
            return ExpressionWriter.ExpressionToString(this.context, expression);
        }
 
        /// <summary>
        /// Ensure that the translated uri has the required minimum version associated with it
        /// </summary>
        /// <param name="major">The major number for the version. </param>
        /// <param name="minor">The minor number for the version. </param>
        private void EnsureMinimumVersion(int major, int minor)
        {
            if (major > this.uriVersion.Major ||
                (major == this.uriVersion.Major && minor > this.uriVersion.Minor))
            {
                this.uriVersion = new Version(major, minor);
            }
        }
    }
}