File: System\Data\Services\Serializers\PlainXmlSerializer.cs
Project: ndp\fx\src\DataWeb\Server\System.Data.Services.csproj (System.Data.Services)
//---------------------------------------------------------------------
// <copyright file="PlainXmlSerializer.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides a serializer for non-reference entities in plain XML.
// </summary>
//
// @owner Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services.Serializers
{
    #region Namespaces.
 
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Data.Services.Providers;
    using System.Diagnostics;
    using System.IO;
    using System.Text;
    using System.Xml;
 
    #endregion Namespaces.
 
    /// <summary>This class serializes primitive and complex type as name/value pairs.</summary>
    internal sealed class PlainXmlSerializer : Serializer
    {
        /// <summary>Writer to which output is sent.</summary>
        private XmlWriter writer;
 
        /// <summary>Initializes a new SyndicationSerializer instance.</summary>
        /// <param name="requestDescription">Description of request.</param>
        /// <param name="absoluteServiceUri">Base URI from which resources should be resolved.</param>
        /// <param name="service">Service with configuration and provider from which metadata should be gathered.</param>
        /// <param name="output">Output stream.</param>
        /// <param name="encoding">Encoding for output.</param>
        internal PlainXmlSerializer(
            RequestDescription requestDescription,
            Uri absoluteServiceUri,
            IDataService service,
            Stream output, 
            Encoding encoding)
            : base(requestDescription, absoluteServiceUri, service, null)
        {
            Debug.Assert(output != null, "output != null");
            Debug.Assert(encoding != null, "encoding != null");
 
            this.writer = XmlUtil.CreateXmlWriterAndWriteProcessingInstruction(output, encoding);
        }
 
        /// <summary>Serializes exception information.</summary>
        /// <param name="args">Description of exception to serialize.</param>
        public override void WriteException(HandleExceptionArgs args)
        {
            ErrorHandler.SerializeXmlError(args, this.writer);
        }
 
        /// <summary>Converts the specified value to a serializable string.</summary>
        /// <param name="value">Non-null value to convert.</param>
        /// <returns>The specified value converted to a serializable string.</returns>
        internal static string PrimitiveToString(object value)
        {
            Debug.Assert(value != null, "value != null");
            string result;
            if (!System.Data.Services.Parsing.WebConvert.TryXmlPrimitiveToString(value, out result))
            {
                throw new InvalidOperationException(Strings.Serializer_CannotConvertValue(value));
            }
 
            return result;
        }
 
        /// <summary>Writes a property with a null value.</summary>
        /// <param name='writer'>XmlWriter to write to.</param>
        /// <param name='propertyName'>Property name.</param>
        /// <param name='expectedTypeName'>Type name for value.</param>
        internal static void WriteNullValue(XmlWriter writer, string propertyName, string expectedTypeName)
        {
            Debug.Assert(writer != null, "writer != null");
            Debug.Assert(propertyName != null, "propertyName != null");
            Debug.Assert(expectedTypeName != null, "expectedTypeName != null");
            WriteStartElementWithType(writer, propertyName, expectedTypeName);
            writer.WriteAttributeString(XmlConstants.AtomNullAttributeName, XmlConstants.DataWebMetadataNamespace, XmlConstants.XmlTrueLiteral);
            writer.WriteEndElement();
        }
 
        /// <summary>Writes the start tag for a property.</summary>
        /// <param name='writer'>XmlWriter to write to.</param>
        /// <param name='propertyName'>Property name.</param>
        /// <param name='propertyTypeName'>Type name for value.</param>
        internal static void WriteStartElementWithType(XmlWriter writer, string propertyName, string propertyTypeName)
        {
            Debug.Assert(writer != null, "writer != null");
            Debug.Assert(propertyName != null, "propertyName != null");
            Debug.Assert(propertyTypeName != null, "expectedType != null");
            Debug.Assert(
               object.ReferenceEquals(propertyTypeName, XmlConstants.EdmStringTypeName) == (propertyTypeName == XmlConstants.EdmStringTypeName),
               "object equality and string equality are the same for Edm.String");
            writer.WriteStartElement(propertyName, XmlConstants.DataWebNamespace);
            if (!object.ReferenceEquals(propertyTypeName, XmlConstants.EdmStringTypeName))
            {
                writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, XmlConstants.DataWebMetadataNamespace, propertyTypeName);
            }
        }
 
        /// <summary>Writes an already-formatted property.</summary>
        /// <param name='writer'>XmlWriter to write to.</param>
        /// <param name='propertyName'>Property name.</param>
        /// <param name='propertyTypeName'>Type name for value.</param>
        /// <param name='propertyText'>Property value in text form.</param>
        internal static void WriteTextValue(XmlWriter writer, string propertyName, string propertyTypeName, string propertyText)
        {
            Debug.Assert(writer != null, "writer != null");
            Debug.Assert(propertyName != null, "propertyName != null");
            WriteStartElementWithType(writer, propertyName, propertyTypeName);
            WebUtil.WriteSpacePreserveAttributeIfNecessary(writer, propertyText);
            writer.WriteString(propertyText);
            writer.WriteEndElement();
        }
 
        /// <summary>Flushes the writer to the underlying stream.</summary>
        protected override void Flush()
        {
            this.writer.Flush();
        }
 
        /// <summary>Writes a single top-level element.</summary>
        /// <param name="expandedResult">Expandd results on the specified <paramref name="element"/>.</param>
        /// <param name="element">Element to write, possibly null.</param>
        protected override void WriteTopLevelElement(IExpandedResult expandedResult, object element)
        {
            Debug.Assert(
                element != null || this.RequestDescription.TargetResourceType != null || this.RequestDescription.TargetKind == RequestTargetKind.OpenProperty,
                "element != null || this.RequestDescription.TargetResourceType != null || this.RequestDescription.TargetKind == RequestTargetKind.OpenProperty");
            Debug.Assert(
                this.RequestDescription.IsSingleResult, 
                "this.RequestDescription.IsSingleResult -- primitive collections not currently supported");
            string propertyName = this.RequestDescription.ContainerName;
            ResourceType resourceType;
            
            if (element == null)
            {
                if (this.RequestDescription.TargetKind == RequestTargetKind.OpenProperty)
                {
                    resourceType = ResourceType.PrimitiveStringResourceType;
                }
                else
                {
                    resourceType = this.RequestDescription.TargetResourceType;
                }
            }
            else
            {
                resourceType = WebUtil.GetResourceType(this.Provider, element);
            }
 
            if (resourceType == null)
            {
                throw new InvalidOperationException(Strings.Serializer_UnsupportedTopLevelType(element.GetType()));
            }
 
            this.WriteValueWithName(element, propertyName, resourceType);
        }
 
        /// <summary>Writes multiple top-level elements, possibly none.</summary>
        /// <param name="expanded">Expanded results for elements.</param>
        /// <param name="elements">Enumerator for elements to write.</param>
        /// <param name="hasMoved">Whether <paramref name="elements"/> was succesfully advanced to the first element.</param>
        protected override void WriteTopLevelElements(IExpandedResult expanded, IEnumerator elements, bool hasMoved)
        {
            Debug.Assert(
                !this.RequestDescription.IsSingleResult,
                "!this.RequestDescription.IsSingleResult -- otherwise WriteTopLevelElement should have been called");
            this.writer.WriteStartElement(this.RequestDescription.ContainerName, XmlConstants.DataWebNamespace);
            while (hasMoved)
            {
                object element = elements.Current;
                ResourceType resourceType = element == null ?
                    this.RequestDescription.TargetResourceType : WebUtil.GetResourceType(this.Provider, element);
                if (resourceType == null)
                {
                    throw new InvalidOperationException(Strings.Serializer_UnsupportedTopLevelType(element.GetType()));
                }
 
                this.WriteValueWithName(element, XmlConstants.XmlCollectionItemElementName, resourceType);
                hasMoved = elements.MoveNext();
            }
 
            this.writer.WriteEndElement();
        }
 
        /// <summary>
        /// Writes the row count.
        /// </summary>
        protected override void WriteRowCount()
        {
            this.writer.WriteStartElement(
                XmlConstants.DataWebMetadataNamespacePrefix,
                XmlConstants.RowCountElement,
                XmlConstants.DataWebMetadataNamespace);
            this.writer.WriteValue(this.RequestDescription.CountValue);
            this.writer.WriteEndElement();
        }
 
        /// <summary>
        /// Write out the uri for the given element
        /// </summary>
        /// <param name="element">element whose uri needs to be written out.</param>
        protected override void WriteLink(object element)
        {
            Debug.Assert(element != null, "element != null");
            this.IncrementSegmentResultCount();
            Uri uri = Serializer.GetUri(element, this.Provider, this.CurrentContainer, this.AbsoluteServiceUri);
            this.writer.WriteStartElement(XmlConstants.UriElementName, XmlConstants.DataWebNamespace);
            this.writer.WriteValue(uri.AbsoluteUri);
            this.writer.WriteEndElement();
        }
 
        /// <summary>
        /// Write out the uri for the given elements
        /// </summary>
        /// <param name="elements">elements whose uri need to be writtne out</param>
        /// <param name="hasMoved">the current state of the enumerator.</param>
        protected override void WriteLinkCollection(IEnumerator elements, bool hasMoved)
        {
            this.writer.WriteStartElement(XmlConstants.LinkCollectionElementName, XmlConstants.DataWebNamespace);
            
            // write count?
            if (this.RequestDescription.CountOption == RequestQueryCountOption.Inline)
            {
                this.WriteRowCount();
            }
 
            object lastObject = null;
            IExpandedResult lastExpandedSkipToken = null;
            while (hasMoved)
            {
                object element = elements.Current;
                IExpandedResult skipToken = null;
                if (element != null)
                {
                    IExpandedResult expanded = element as IExpandedResult;
                    if (expanded != null)
                    {
                        element = GetExpandedElement(expanded);
                        skipToken = this.GetSkipToken(expanded);
                    }
                }
                
                this.WriteLink(element);
                hasMoved = elements.MoveNext();
                lastObject = element;
                lastExpandedSkipToken = skipToken;
            }
 
            if (this.NeedNextPageLink(elements))
            {
                this.WriteNextPageLink(lastObject, lastExpandedSkipToken, this.RequestDescription.ResultUri);
            }
 
            this.writer.WriteEndElement();
        }
 
        /// <summary>
        /// Writes the next page link to the current xml writer corresponding to the feed
        /// </summary>
        /// <param name="lastElement">Object that will contain the keys for skip token</param>
        /// <param name="expandedResult">The <see cref="IExpandedResult"/> value of the $skiptoken property of the object being written</param>
        /// <param name="absoluteUri">Absolute URI for the result</param>
        private void WriteNextPageLink(object lastElement, IExpandedResult expandedResult, Uri absoluteUri)
        {
            this.writer.WriteStartElement(XmlConstants.NextElementName, XmlConstants.DataWebNamespace);
            this.writer.WriteValue(this.GetNextLinkUri(lastElement, expandedResult, absoluteUri));
            this.writer.WriteEndElement();
        }
 
        /// <summary>Writes all the properties of the specified resource or complex object.</summary>
        /// <param name="element">Resource or complex object with properties to write out.</param>
        /// <param name="resourceType">Resource type describing the element type.</param>
        private void WriteObjectProperties(object element, ResourceType resourceType)
        {
            Debug.Assert(element != null, "element != null");
            Debug.Assert(resourceType != null, "resourceType != null");
 
            foreach (ResourceProperty property in resourceType.Properties)
            {
                string propertyName = property.Name;
                object propertyValue = WebUtil.GetPropertyValue(this.Provider, element, resourceType, property, null);
                bool needPop = this.PushSegmentForProperty(property);
 
                if (property.TypeKind == ResourceTypeKind.ComplexType)
                {
                    this.WriteComplexObjectValue(propertyValue, propertyName, property.ResourceType);
                }
                else
                {
                    Debug.Assert(property.TypeKind == ResourceTypeKind.Primitive, "property.TypeKind == ResourceTypeKind.Primitive");
                    this.WritePrimitiveValue(propertyValue, property.Name, property.ResourceType);
                }
 
                this.PopSegmentName(needPop);
            }
 
            if (resourceType.IsOpenType)
            {
                IEnumerable<KeyValuePair<string, object>> properties = this.Provider.GetOpenPropertyValues(element);
                foreach (KeyValuePair<string, object> property in properties)
                {
                    string propertyName = property.Key;
                    object value = property.Value;
                    if (value == null || value == DBNull.Value)
                    {
                        continue;
                    }
 
                    ResourceType propertyResourceType = WebUtil.GetResourceType(this.Provider, value);
                    if (propertyResourceType == null)
                    {
                        continue;
                    }
 
                    if (propertyResourceType.ResourceTypeKind == ResourceTypeKind.Primitive)
                    {
                        this.WritePrimitiveValue(value, propertyName, propertyResourceType);
                    }
                    else
                    {
                        if (propertyResourceType.ResourceTypeKind == ResourceTypeKind.ComplexType)
                        {
                            Debug.Assert(propertyResourceType.InstanceType == value.GetType(), "propertyResourceType.Type == valueType");
                            this.WriteComplexObjectValue(value, propertyName, propertyResourceType);
                        }
                        else
                        {
                            Debug.Assert(propertyResourceType.ResourceTypeKind == ResourceTypeKind.EntityType, "EntityType expected.");
 
                            // Open navigation properties are not supported on OpenTypes
                            throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(propertyName));
                        }
                    }
                }
            }
        }
 
        /// <summary>Writes a property with a primitive value.</summary>
        /// <param name='element'>Value.</param>
        /// <param name='propertyName'>Property name.</param>
        /// <param name='type'>Type for element.</param>
        private void WritePrimitiveValue(object element, string propertyName, ResourceType type)
        {
            Debug.Assert(propertyName != null, "propertyName != null");
            Debug.Assert(type != null, "type != null");
 
            if (element == null)
            {
                WriteNullValue(this.writer, propertyName, type.FullName);
                return;
            }
 
            string propertyText = PrimitiveToString(element);
            WriteTextValue(this.writer, propertyName, type.FullName, propertyText);
        }
 
        /// <summary>Writes the value of a complex object.</summary>
        /// <param name="element">Element to write.</param>
        /// <param name="propertyName">Property name.</param>
        /// <param name="expectedType">Type for element.</param>
        private void WriteComplexObjectValue(object element, string propertyName, ResourceType expectedType)
        {
            Debug.Assert(!String.IsNullOrEmpty(propertyName), "!String.IsNullOrEmpty(propertyName)");
            Debug.Assert(expectedType != null, "expectedType != null");
            Debug.Assert(expectedType.ResourceTypeKind == ResourceTypeKind.ComplexType, "Must be complex type");
 
            if (element == null)
            {
                WriteNullValue(this.writer, propertyName, expectedType.FullName);
                return;
            }
 
            // Non-value complex types may form a cycle.
            // PERF: we can keep a single element around and save the HashSet initialization
            // until we find a second complex type - this saves the allocation on trees
            // with shallow (single-level) complex types.
            this.RecurseEnter();
            Debug.Assert(!expectedType.InstanceType.IsValueType, "!expectedType.Type.IsValueType -- checked in the resource type constructor.");
            if (this.AddToComplexTypeCollection(element))
            {
                ResourceType resourceType = WebUtil.GetNonPrimitiveResourceType(this.Provider, element);
                WriteStartElementWithType(this.writer, propertyName, resourceType.FullName);
                this.WriteObjectProperties(element, resourceType);
                this.writer.WriteEndElement();
                this.RemoveFromComplexTypeCollection(element);
            }
            else
            {
                throw new InvalidOperationException(Strings.Serializer_LoopsNotAllowedInComplexTypes(propertyName));
            }
 
            this.RecurseLeave();
        }
 
        /// <summary>Writes the <paramref name="element"/> value with the specified name.</summary>
        /// <param name="element">Element to write out.</param>
        /// <param name="propertyName">Property name for element.</param>
        /// <param name="resourceType">Type of resource to write.</param>
        private void WriteValueWithName(object element, string propertyName, ResourceType resourceType)
        {
            Debug.Assert(propertyName != null, "propertyName != null");
            Debug.Assert(resourceType != null, "resourceType != null");
 
            if (resourceType.ResourceTypeKind == ResourceTypeKind.Primitive)
            {
                this.WritePrimitiveValue(element, propertyName, resourceType);
            }
            else
            {
                Debug.Assert(
                    resourceType.ResourceTypeKind == ResourceTypeKind.ComplexType,
                    "resourceType.ResourceTypeKind == ResourceTypeKind.ComplexType -- POX doesn't support " + resourceType.ResourceTypeKind);
                this.WriteComplexObjectValue(element, propertyName, resourceType);
            }
        }
    }
}