/ 4.0 / 4.0 / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / fx / src / DataWeb / Server / System / Data / Services / Serializers / SyndicationSerializer.cs / 1305376 / SyndicationSerializer.cs
//----------------------------------------------------------------------
//
// 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.Common;
using System.Data.Services.Providers;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.ServiceModel.Syndication;
using System.Text;
using System.Xml;
using System.Xml.Linq;
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);
}
}
/// 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.
internal 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);
}
/// 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)
{
bool needPop = this.PushSegmentForRoot();
this.WriteEntryElement(
expanded,
element,
this.RequestDescription.TargetResourceType,
this.RequestDescription.ResultUri,
this.RequestDescription.ContainerName,
this.resultItem);
this.PopSegmentName(needPop);
}
else
{
Debug.Assert(
this.RequestDescription.TargetSource == RequestTargetSource.Property,
"TargetSource(" + this.RequestDescription.TargetSource + ") == Property -- otherwise control shouldn't be here.");
ResourceType resourcePropertyType;
if (this.RequestDescription.TargetKind == RequestTargetKind.OpenProperty)
{
resourcePropertyType = (element == null) ? ResourceType.PrimitiveStringResourceType : WebUtil.GetResourceType(this.Provider, element);
if (resourcePropertyType == null)
{
Type propertyType = element == null ? typeof(string) : element.GetType();
throw new InvalidOperationException(Strings.Serializer_UnsupportedTopLevelType(propertyType));
}
}
else
{
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.");
bool needPop = this.PushSegmentForRoot();
this.WriteEntryElement(
expanded, // expanded
element, // element
resourcePropertyType, // expectedType
this.RequestDescription.ResultUri, // absoluteUri
this.RequestDescription.ContainerName, // relativeUri
this.resultItem); // target
this.PopSegmentName(needPop);
}
// 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 (this.RequestDescription.TargetKind != RequestTargetKind.OpenProperty &&
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;
// support for $count
if (this.RequestDescription.CountOption == RequestQueryCountOption.Inline)
{
this.WriteRowCount();
}
bool needPop = this.PushSegmentForRoot();
this.WriteFeedElements(
expanded,
elements,
this.RequestDescription.TargetResourceType,
title, // title
this.RequestDescription.ResultUri, // absoluteUri
relativeUri, // relativeUri
hasMoved, // hasMoved
this.resultFeed, // feed
false);
this.PopSegmentName(needPop);
SyndicationFeedFormatter formatter = this.factory.CreateSyndicationFeedFormatter(this.resultFeed);
formatter.WriteTo(this.writer);
}
///
/// Write out the entry count
///
protected override void WriteRowCount()
{
XElement rowCountElement = new XElement(
XName.Get(XmlConstants.RowCountElement, XmlConstants.DataWebMetadataNamespace),
RequestDescription.CountValue);
this.resultFeed.ElementExtensions.Add(rowCountElement);
}
///
/// 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));
}
///
/// Write the link relation element
///
/// title for the current element
/// link relation for the self uri
/// relative uri for the current element
/// Item to write to.
/// List of custom attributes to add to the link element
private static void WriteLinkRelations(string title, string linkRelation, string relativeUri, SyndicationItem item, params KeyValuePair[] attributeExtensions)
{
Debug.Assert(item != null, "item != null");
Debug.Assert(relativeUri != null, "relativeUri != null");
// Write the link relation element
var link = new SyndicationLink();
link.RelationshipType = linkRelation;
link.Title = title;
link.Uri = new Uri(relativeUri, UriKind.Relative);
foreach (KeyValuePair attributeExtension in attributeExtensions)
{
link.AttributeExtensions.Add(attributeExtension.Key, attributeExtension.Value);
}
item.Links.Add(link);
}
///
/// Checks if a particular property value should be skipped from the content section due to
/// EntityProperty mappings for friendly feeds
///
/// Current root segment in the source tree for a resource type
/// Name of the property being checked for
/// true if skipping of property value is needed, false otherwise
private static bool EpmNeedToSkip(EpmSourcePathSegment currentSourceRoot, String propertyName)
{
if (currentSourceRoot != null)
{
EpmSourcePathSegment epmProperty = currentSourceRoot.SubProperties.Find(subProp => subProp.PropertyName == propertyName);
if (epmProperty != null)
{
Debug.Assert(epmProperty.SubProperties.Count == 0, "Complex type added as leaf node in EPM tree.");
Debug.Assert(epmProperty.EpmInfo != null, "Found a non-leaf property for which EpmInfo is not set.");
Debug.Assert(epmProperty.EpmInfo.Attribute != null, "Attribute should always be initialized for EpmInfo.");
if (epmProperty.EpmInfo.Attribute.KeepInContent == false)
{
return true;
}
}
}
return false;
}
///
/// Obtains the child EPM segment corresponding to the given
///
/// Current root segment
/// Name of property
/// Child segment or null if there is not segment corresponding to the given
private static EpmSourcePathSegment EpmGetComplexPropertySegment(EpmSourcePathSegment currentSourceRoot, String propertyName)
{
if (currentSourceRoot != null)
{
return currentSourceRoot.SubProperties.Find(subProp => subProp.PropertyName == propertyName);
}
else
{
return null;
}
}
/// 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.
/// Epm source sub-tree corresponding to
private void WriteComplexObjectValue(object element, string propertyName, ResourceType expectedType, string relativeUri, DictionaryContent content, EpmSourcePathSegment currentSourceRoot)
{
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.
Debug.Assert(!expectedType.IsMediaLinkEntry, "!expectedType.IsMediaLinkEntry");
DictionaryContent valueProperties = new DictionaryContent(expectedType.Properties.Count);
Debug.Assert(!expectedType.InstanceType.IsValueType, "!expectedType.Type.IsValueType -- checked in the resource type constructor.");
if (element == null)
{
content.AddNull(expectedType.FullName, propertyName);
}
else
{
if (this.AddToComplexTypeCollection(element))
{
ResourceType resourceType = WebUtil.GetNonPrimitiveResourceType(this.Provider, element);
this.WriteObjectProperties(null, element, resourceType, null, relativeUri, null, valueProperties, currentSourceRoot);
if (!valueProperties.IsEmpty)
{
content.Add(propertyName, resourceType.FullName, valueProperties);
}
this.RemoveFromComplexTypeCollection(element);
}
else
{
throw new InvalidOperationException(Strings.Serializer_LoopsNotAllowedInComplexTypes(propertyName));
}
}
}
/// 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, ResourceType expectedType, Uri absoluteUri, string relativeUri, SyndicationItem target)
{
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;
if (expectedType == null)
{
// If the request uri is targetting some open type properties, then we don't know the type of the resource
// Hence we assume it to be of object type. The reason we do this is that if the value is null, there is
// no way to know what the type of the property would be, and then we write it out as object. If the value
// is not null, then we do get the resource type from the instance and write out the actual resource type.
title = typeof(object).Name;
fullName = typeof(object).FullName;
}
else
{
title = expectedType.Name;
fullName = expectedType.FullName;
}
target.Title = new TextSyndicationContent(String.Empty);
if (element == null)
{
SetEntryTypeName(target, fullName);
target.AttributeExtensions[QualifiedNullAttribute] = XmlConstants.XmlTrueLiteral;
this.WriteOtherElements(
element,
expectedType,
title,
absoluteUri,
relativeUri,
null,
target);
// Don't know when we hit this code path, keeping existing behaviour in this case
target.Authors.Add(EmptyPerson);
}
else
{
absoluteUri = Serializer.GetUri(element, this.Provider, this.CurrentContainer, this.AbsoluteServiceUri);
Debug.Assert(absoluteUri.AbsoluteUri.StartsWith(this.AbsoluteServiceUri.AbsoluteUri, StringComparison.Ordinal), "absoluteUri.AbsoluteUri.StartsWith(this.AbsoluteServiceUri.AbsoluteUri, StringComparison.Ordinal))");
relativeUri = absoluteUri.AbsoluteUri.Substring(this.AbsoluteServiceUri.AbsoluteUri.Length);
ResourceType actualResourceType = WebUtil.GetNonPrimitiveResourceType(this.Provider, element);
string mediaETag = null;
Uri readStreamUri = null;
string mediaContentType = null;
if (actualResourceType.IsMediaLinkEntry)
{
this.Service.StreamProvider.GetStreamDescription(element, this.Service.OperationContext, relativeUri, out mediaETag, out readStreamUri, out mediaContentType);
}
SetEntryTypeName(target, actualResourceType.FullName);
this.WriteOtherElements(
element,
actualResourceType,
title,
absoluteUri,
relativeUri,
mediaETag,
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(actualResourceType.Properties.Count);
using (EpmContentSerializer epmSerializer = new EpmContentSerializer(actualResourceType, element, target, this.Provider))
{
this.WriteObjectProperties(expanded, element, actualResourceType, absoluteUri, relativeUri, target, content, actualResourceType.HasEntityPropertyMappings ? actualResourceType.EpmSourceTree.Root : null);
epmSerializer.Serialize(content, this.Provider);
}
if (actualResourceType.IsMediaLinkEntry)
{
// Write
Debug.Assert(readStreamUri != null, "readStreamUri != null");
Debug.Assert(!string.IsNullOrEmpty(mediaContentType), "!string.IsNullOrEmpty(mediaContentType)");
target.Content = new UrlSyndicationContent(readStreamUri, mediaContentType);
if (!content.IsEmpty)
{
// Since UrlSyndicationContent must have empty content, we write the node as SyndicationElementExtension.
target.ElementExtensions.Add(content.GetPropertyContentsReader());
}
}
else
{
target.Content = content;
}
}
#if ASTORIA_FF_CALLBACKS
this.Service.InternalOnWriteItem(target, element);
#endif
}
///
/// 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.
/// If set to true the function should dispose the elements enumerator when it's done
/// with it. Not in the case this method fails though.
private void WriteFeedElements(
IExpandedResult expanded,
IEnumerator elements,
ResourceType expectedType,
string title,
Uri absoluteUri,
string relativeUri,
bool hasMoved,
SyndicationFeed feed,
bool disposeElementsOnSuccess)
{
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);
if (!hasMoved)
{
// ATOM specification: if a feed contains no entries, then the feed should have at least one Author tag
feed.Authors.Add(EmptyPerson);
}
// 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(),
(o, e) => this.WriteNextPageLink(o, e, absoluteUri),
disposeElementsOnSuccess);
#if ASTORIA_FF_CALLBACKS
this.Service.InternalOnWriteFeed(feed);
#endif
}
///
/// Writes the next page link to the current xml writer corresponding to the feed
///
/// Object that will contain the keys for skip token
/// The of the $skiptoken property of the object being written
/// Absolute URI for the result
private void WriteNextPageLink(object lastElement, IExpandedResult expandedResult, Uri absoluteUri)
{
this.writer.WriteStartElement("link", XmlConstants.AtomNamespace);
this.writer.WriteAttributeString("rel", "next");
this.writer.WriteAttributeString("href", this.GetNextLinkUri(lastElement, expandedResult, absoluteUri));
this.writer.WriteEndElement();
}
/// 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.
/// Delegate that writes the next page link if necessity arises
/// If set to true the function should dispose the elements enumerator (always).
/// An object that can enumerate syndication items.
private IEnumerable DeferredFeedItems(
IExpandedResult expanded,
IEnumerator elements,
ResourceType expectedType,
bool hasMoved,
object activeSegmentNames,
Action