//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// Provides a serializer that creates syndication objects and
// then formatts them.
//
//
// @owner [....]
//---------------------------------------------------------------------
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.Globalization;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.ServiceModel.Syndication;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
#endregion Namespaces.
/// Serializes results into System.ServiceModel.Syndication objects, which can then be formatted.
internal sealed class SyndicationSerializer : Serializer
{
#region Fields.
/// Namespace-qualified attribute for null value annotations.
internal static readonly XmlQualifiedName QualifiedNullAttribute = new XmlQualifiedName(XmlConstants.AtomNullAttributeName, XmlConstants.DataWebMetadataNamespace);
/// Empty person singleton.
private static readonly SyndicationPerson EmptyPerson = new SyndicationPerson(null, String.Empty, null);
/// Namespace-qualified namespace prefix for the DataWeb namespace.
private static readonly XmlQualifiedName QualifiedDataWebPrefix = new XmlQualifiedName(XmlConstants.DataWebNamespacePrefix, XmlConstants.XmlNamespacesNamespace);
/// Namespace-qualified namespace prefix for the DataWebMetadata namespace.
private static readonly XmlQualifiedName QualifiedDataWebMetadataPrefix = new XmlQualifiedName(XmlConstants.DataWebMetadataNamespacePrefix, XmlConstants.XmlNamespacesNamespace);
/// Factory for syndication formatter implementation.
private readonly SyndicationFormatterFactory factory;
/// Last updated time for elements.
///
/// While this is currently an arbitrary decision, it at least saves us from re-querying the system time
/// every time an item is generated.
///
private readonly DateTimeOffset lastUpdatedTime = DateTimeOffset.UtcNow;
/// Writer to which output is sent.
private readonly XmlWriter writer;
/// Top-level feed being built.
private SyndicationFeed resultFeed;
/// Top-level item being built.
private SyndicationItem resultItem;
#endregion Fields.
/// Initializes a new SyndicationSerializer instance.
/// Request description.
/// Absolute URI to the service entry point.
/// Service with configuration and provider from which metadata should be gathered.
/// Stream to write to.
/// Encoding for text in output stream.
/// HTTP ETag header value.
/// Factory for formatter objects.
internal SyndicationSerializer(
RequestDescription requestDescription,
Uri absoluteServiceUri,
IDataService service,
Stream output,
Encoding encoding,
string etag,
SyndicationFormatterFactory factory)
: base(requestDescription, absoluteServiceUri, service, etag)
{
Debug.Assert(service != null, "service != null");
Debug.Assert(output != null, "output != null");
Debug.Assert(encoding != null, "encoding != null");
Debug.Assert(factory != null, "factory != null");
this.factory = factory;
this.writer = factory.CreateWriter(output, encoding);
}
/// Serializes exception information.
/// Description of exception to serialize.
public override void WriteException(HandleExceptionArgs args)
{
ErrorHandler.SerializeXmlError(args, this.writer);
}
/// Writes a primitive value to the specified output.
/// Primitive value to write.
/// name of the property whose value needs to be written
/// Type name of the property
/// Content dictionary to which the value should be written.
internal static void WritePrimitiveValue(object primitive, string propertyName, string expectedTypeName, DictionaryContent content)
{
Debug.Assert(!String.IsNullOrEmpty(propertyName), "!String.IsNullOrEmpty(propertyName)");
Debug.Assert(expectedTypeName != null, "expectedTypeName != null");
if (primitive == null)
{
content.AddNull(expectedTypeName, propertyName);
}
else
{
string primitiveString = PlainXmlSerializer.PrimitiveToString(primitive);
Debug.Assert(primitiveString != null, "primitiveString != null");
content.Add(propertyName, expectedTypeName, primitiveString);
}
}
/// Flushes the writer to the underlying stream.
protected override void Flush()
{
this.writer.Flush();
}
/// Writes a single top-level element.
/// Expanded properties for the result.
/// Element to write, possibly null.
protected override void WriteTopLevelElement(IExpandedResult expanded, object element)
{
Debug.Assert(this.RequestDescription.IsSingleResult, "this.RequestDescription.SingleResult");
Debug.Assert(element != null, "element != null");
this.resultItem = new SyndicationItem();
this.resultItem.BaseUri = this.AbsoluteServiceUri;
IncludeCommonNamespaces(this.resultItem.AttributeExtensions);
if (this.RequestDescription.TargetSource == RequestTargetSource.EntitySet ||
this.RequestDescription.TargetSource == RequestTargetSource.ServiceOperation)
{
this.PushSegmentForRoot();
this.WriteEntryElement(
expanded,
element,
this.RequestDescription.TargetElementType,
this.RequestDescription.ResultUri,
this.RequestDescription.ContainerName,
this.resultItem);
this.PopSegmentName();
}
else
{
Debug.Assert(
this.RequestDescription.TargetSource == RequestTargetSource.Property,
"TargetSource(" + this.RequestDescription.TargetSource + ") == Property -- otherwise control shouldn't be here.");
ResourceType resourcePropertyType;
#if ASTORIA_OPEN_OBJECT
if (this.RequestDescription.TargetKind == RequestTargetKind.OpenProperty)
{
Type propertyType = element == null ? typeof(string) : element.GetType();
resourcePropertyType = this.Provider.GetResourceType(propertyType);
if (resourcePropertyType == null)
{
throw new InvalidOperationException(Strings.Serializer_UnsupportedTopLevelType(propertyType));
}
}
else
#endif
{
Debug.Assert(this.RequestDescription.Property != null, "this.RequestDescription.Property - otherwise Property source set with no Property specified.");
ResourceProperty property = this.RequestDescription.Property;
resourcePropertyType = property.ResourceType;
}
Debug.Assert(
resourcePropertyType.ResourceTypeKind == ResourceTypeKind.EntityType,
"Open ResourceTypeKind == EnityType -- temporarily, because ATOM is the only implemented syndication serializer and doesn't support it.");
this.PushSegmentForRoot();
this.WriteEntryElement(
expanded, // expanded
element, // element
resourcePropertyType.Type, // expectedType
this.RequestDescription.ResultUri, // absoluteUri
this.RequestDescription.ContainerName, // relativeUri
this.resultItem); // target
this.PopSegmentName();
}
// Since the element is not equal to null, the factory should never return null
SyndicationItemFormatter formatter = this.factory.CreateSyndicationItemFormatter(this.resultItem);
formatter.WriteTo(this.writer);
}
/// Writes multiple top-level elements, possibly none.
/// Expanded properties for the result.
/// Enumerator for elements to write.
/// Whether was succesfully advanced to the first element.
protected override void WriteTopLevelElements(IExpandedResult expanded, IEnumerator elements, bool hasMoved)
{
Debug.Assert(elements != null, "elements != null");
Debug.Assert(!this.RequestDescription.IsSingleResult, "!this.RequestDescription.SingleResult");
string title;
if (
#if ASTORIA_OPEN_OBJECT
this.RequestDescription.TargetKind != RequestTargetKind.OpenProperty &&
#endif
this.RequestDescription.TargetSource == RequestTargetSource.Property)
{
title = this.RequestDescription.Property.Name;
}
else
{
title = this.RequestDescription.ContainerName;
}
this.resultFeed = new SyndicationFeed();
IncludeCommonNamespaces(this.resultFeed.AttributeExtensions);
this.resultFeed.BaseUri = RequestUriProcessor.AppendEscapedSegment(this.AbsoluteServiceUri, "");
string relativeUri = this.RequestDescription.LastSegmentInfo.Identifier;
this.PushSegmentForRoot();
this.WriteFeedElements(
expanded,
elements,
this.RequestDescription.TargetElementType,
title, // title
this.RequestDescription.ResultUri, // absoluteUri
relativeUri, // relativeUri
hasMoved, // hasMoved
this.resultFeed); // feed
this.PopSegmentName();
SyndicationFeedFormatter formatter = this.factory.CreateSyndicationFeedFormatter(this.resultFeed);
formatter.WriteTo(this.writer);
}
///
/// Write out the uri for the given element
///
/// element whose uri needs to be written out.
protected override void WriteLink(object element)
{
throw Error.NotImplemented();
}
///
/// Write out the uri for the given elements
///
/// elements whose uri need to be writtne out
/// the current state of the enumerator.
protected override void WriteLinkCollection(IEnumerator elements, bool hasMoved)
{
throw Error.NotImplemented();
}
/// Ensures that common namespaces are included in the topmost tag.
/// Attribute extensions to write namespaces to.
///
/// This method should be called by any method that may write a
/// topmost element tag.
///
private static void IncludeCommonNamespaces(Dictionary attributeExtensions)
{
attributeExtensions.Add(QualifiedDataWebPrefix, XmlConstants.DataWebNamespace);
attributeExtensions.Add(QualifiedDataWebMetadataPrefix, XmlConstants.DataWebMetadataNamespace);
}
/// Sets the type name for the specified syndication entry.
/// Item on which to set the type name.
/// Full type name for the entry.
private static void SetEntryTypeName(SyndicationItem item, string fullName)
{
Debug.Assert(item != null, "item != null");
item.Categories.Add(new SyndicationCategory(fullName, XmlConstants.DataWebSchemeNamespace, null));
}
/// Writes an Atom link element.
/// relation of the link element with the parent element
/// title of the deferred element
/// uri for the deferred element
/// link type for the deferred element
/// Item to write link in.
private static void WriteDeferredContentElement(string linkRelation, string title, string href, string linkType, SyndicationItem item)
{
Debug.Assert(linkRelation != null, "linkRelation != null");
Debug.Assert(item != null, "item != null");
Debug.Assert(linkType != null, "linkType != null");
SyndicationLink link = new SyndicationLink();
link.RelationshipType = linkRelation;
link.Title = title;
link.Uri = new Uri(href, UriKind.RelativeOrAbsolute);
link.MediaType = linkType;
item.Links.Add(link);
}
/// Writes the value of a complex object.
/// Element to write.
/// name of the property whose value needs to be written
/// expected type of the property
/// relative uri for the complex type element
/// Content to write to.
private void WriteComplexObjectValue(object element, string propertyName, ResourceType expectedType, string relativeUri, DictionaryContent content)
{
Debug.Assert(!String.IsNullOrEmpty(propertyName), "!String.IsNullOrEmpty(propertyName)");
Debug.Assert(expectedType != null, "expectedType != null");
Debug.Assert(!String.IsNullOrEmpty(relativeUri), "!String.IsNullOrEmpty(relativeUri)");
Debug.Assert(expectedType.ResourceTypeKind == ResourceTypeKind.ComplexType, "Must be complex type");
Debug.Assert(content != null, "content != null");
// 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.
DictionaryContent valueProperties = new DictionaryContent(expectedType.Properties.Count);
Debug.Assert(!expectedType.Type.IsValueType, "!expectedType.Type.IsValueType -- support for this was removed.");
if (element == null)
{
content.AddNull(expectedType.FullName, propertyName);
}
else
{
Type elementType = element.GetType();
if (this.AddToComplexTypeCollection(element))
{
ResourceType resourceType = this.Provider.GetResourceType(elementType);
content.Add(propertyName, resourceType.FullName, valueProperties);
this.WriteObjectProperties(null, element, resourceType, null, relativeUri, null, valueProperties);
this.RemoveFromComplexTypeCollection(element);
}
else
{
throw new InvalidOperationException(Strings.DataServiceException_GeneralError);
}
}
}
/// Write the entry element.
/// Expanded result provider for the specified .
/// element representing the entry element
/// expected type of the entry element
/// absolute uri for the entry element
/// relative uri for the entry element
/// Target to write to.
private void WriteEntryElement(IExpandedResult expanded, object element, Type expectedType, Uri absoluteUri, string relativeUri, SyndicationItem target)
{
Debug.Assert(expectedType != null, "expectedType != null");
Debug.Assert(element != null || (absoluteUri != null && !String.IsNullOrEmpty(relativeUri)), "Uri's must be specified for null values");
Debug.Assert(target != null, "target != null");
this.IncrementSegmentResultCount();
string title, fullName;
ResourceType resourceType = this.Provider.GetResourceType(expectedType);
if (resourceType == null)
{
Debug.Assert(expectedType == typeof(object), "only object type expected here");
title = expectedType.Name;
fullName = expectedType.FullName;
}
else
{
title = resourceType.Name;
fullName = resourceType.FullName;
}
if (element == null)
{
SetEntryTypeName(target, fullName);
target.AttributeExtensions[QualifiedNullAttribute] = XmlConstants.XmlTrueLiteral;
this.WriteOtherElements(
title,
XmlConstants.AtomEditRelationAttributeValue,
absoluteUri,
relativeUri,
target);
}
else
{
absoluteUri = Serializer.GetUri(element, this.Provider, this.GetContainerForCurrent(element), this.AbsoluteServiceUri);
string[] segments = absoluteUri.Segments;
Debug.Assert(segments.Length > 0, "segments > 0 -- a path to an entry should have at least one segment");
relativeUri = segments[segments.Length - 1];
Type elementType = element.GetType();
resourceType = this.Provider.GetResourceType(elementType);
SetEntryTypeName(target, resourceType.FullName);
this.WriteOtherElements(
title,
XmlConstants.AtomEditRelationAttributeValue,
absoluteUri,
relativeUri,
target);
// Write the etag property, if the type has etag properties
string etag = this.GetETagValue(element);
if (etag != null)
{
target.AttributeExtensions[new XmlQualifiedName(XmlConstants.AtomETagAttributeName, XmlConstants.DataWebMetadataNamespace)]
= etag;
}
DictionaryContent content = new DictionaryContent(resourceType.Properties.Count);
target.Content = content;
this.WriteObjectProperties(expanded, element, resourceType, absoluteUri, relativeUri, target, content);
}
}
///
/// Writes the feed element for the atom payload
///
/// Expanded properties for the result.
/// collection of entries in the feed element
/// expectedType of the elements in the collection
/// title of the feed element
/// absolute uri representing the feed element
/// relative uri representing the feed element
/// whether the enumerator has successfully moved to the first element
/// Feed to write to.
private void WriteFeedElements(IExpandedResult expanded, IEnumerator elements, Type expectedType, string title, Uri absoluteUri, string relativeUri, bool hasMoved, SyndicationFeed feed)
{
Debug.Assert(feed != null, "feed != null");
// Write the other elements for the feed
feed.Id = absoluteUri.AbsoluteUri;
feed.Title = new TextSyndicationContent(title);
var uri = new Uri(relativeUri, UriKind.Relative);
var link = new SyndicationLink(uri, XmlConstants.AtomSelfRelationAttributeValue, title, null, 0L);
feed.Links.Add(link);
// Instead of looping, create an item that will defer the production of SyndicationItem instances.
// PERF: consider optimizing out empty collections when hasMoved is false.
feed.Items = this.DeferredFeedItems(expanded, elements, expectedType, hasMoved, this.SaveSegmentNames());
}
/// Provides an enumeration of deferred feed items.
/// Expanded properties for the result.
/// Elements to enumerate.
/// Expected type of elements.
/// Whether the enumerator moved to the first element.
/// The segment names active at this point in serialization.
/// An object that can enumerate syndication items.
private IEnumerable DeferredFeedItems(IExpandedResult expanded, IEnumerator elements, Type expectedType, bool hasMoved, object activeSegmentNames)
{
object savedSegmentNames = this.SaveSegmentNames();
this.RestoreSegmentNames(activeSegmentNames);
while (hasMoved)
{
object o = elements.Current;
if (o != null)
{
SyndicationItem target = new SyndicationItem();
if (o is IExpandedResult)
{
expanded = (IExpandedResult)o;
o = expanded.ExpandedElement;
}
this.WriteEntryElement(expanded, o, expectedType, null, null, target);
yield return target;
}
hasMoved = elements.MoveNext();
}
this.RestoreSegmentNames(savedSegmentNames);
}
///
/// Write entry/feed elements, except the content element and related links
///
/// title for the current element
/// link relation for the self uri
/// absolute uri for the current element
/// relative uri for the current element
/// Item to write to.
private void WriteOtherElements(string title, string linkRelation, Uri absoluteUri, string relativeUri, SyndicationItem item)
{
Debug.Assert(item != null, "item != null");
Debug.Assert(relativeUri != null, "relativeUri != null");
item.Id = absoluteUri.AbsoluteUri;
item.LastUpdatedTime = this.lastUpdatedTime;
item.Authors.Add(EmptyPerson);
item.Title = new TextSyndicationContent(String.Empty);
// Write the link relation element
var link = new SyndicationLink();
link.RelationshipType = linkRelation;
link.Title = title;
link.Uri = new Uri(relativeUri, UriKind.Relative);
item.Links.Add(link);
}
/// Writes all the properties of the specified resource or complex object.
/// Expanded properties for the result.
/// Resource or complex object with properties to write out.
/// resourceType containing metadata about the current custom object
/// absolute uri for the given resource
/// relative uri for the given resource
/// Item in which to place links / expansions.
/// Content in which to place values.
private void WriteObjectProperties(IExpandedResult expanded, object customObject, ResourceType resourceType, Uri absoluteUri, string relativeUri, SyndicationItem item, DictionaryContent content)
{
Debug.Assert(customObject != null, "customObject != null");
Debug.Assert(resourceType != null, "resourceType != null");
Debug.Assert(resourceType.Type.IsAssignableFrom(customObject.GetType()), "resourceType.Type.IsAssignableFrom(customObject.GetType())");
Debug.Assert(!String.IsNullOrEmpty(relativeUri), "!String.IsNullOrEmpty(relativeUri)");
Debug.Assert(
absoluteUri != null || resourceType.ResourceTypeKind != ResourceTypeKind.EntityType,
"absoluteUri != null || resourceType.ResourceTypeKind != ResourceTypeKind.EntityType");
List navProperties = null;
List