|
//---------------------------------------------------------------------
// <copyright file="Deserializer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// Provides a base deserializer for all deserializers.
// </summary>
//
// @owner Microsoft
//---------------------------------------------------------------------
namespace System.Data.Services.Serializers
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Services.Parsing;
using System.Data.Services.Providers;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
/// <summary>
/// Provides a abstract base deserializer class
/// </summary>
internal abstract class Deserializer : IDisposable
{
/// <summary>Maximum recursion limit on deserializer.</summary>
private const int RecursionLimit = 100;
/// <summary>Data service for which the deserializer will act.</summary>
private readonly IDataService service;
/// <summary>Tracker for actions taken during deserialization.</summary>
private readonly UpdateTracker tracker;
/// <summary> Indicates whether the payload is for update or not </summary>
private readonly bool update;
/// <summary>Depth of recursion.</summary>
private int recursionDepth;
/// <summary>number of resources (entity or complex type) referred in this request.</summary>
private int objectCount;
/// <summary>Request description for the top level target entity.</summary>
private RequestDescription description;
/// <summary>Initializes a new <see cref="Deserializer"/> for the specified stream.</summary>
/// <param name="update">indicates whether this is a update operation or not</param>
/// <param name="dataService">Data service for which the deserializer will act.</param>
/// <param name="tracker">Tracker to use for modifications.</param>
internal Deserializer(bool update, IDataService dataService, UpdateTracker tracker)
{
Debug.Assert(dataService != null, "dataService != null");
this.service = dataService;
this.tracker = tracker;
this.update = update;
}
/// <summary>Initializes a new <see cref="Deserializer"/> based on a different one.</summary>
/// <param name="parent">Parent deserializer for the new instance.</param>
internal Deserializer(Deserializer parent)
{
Debug.Assert(parent != null, "parent != null");
this.recursionDepth = parent.recursionDepth;
this.service = parent.service;
this.tracker = parent.tracker;
this.update = parent.update;
}
/// <summary>Tracker for actions taken during deserialization.</summary>
internal UpdateTracker Tracker
{
[DebuggerStepThrough]
get { return this.tracker; }
}
/// <summary>returns the content format for the deserializer</summary>
protected abstract ContentFormat ContentFormat
{
get;
}
/// <summary>Data service for which the deserializer will act.</summary>
protected IDataService Service
{
[DebuggerStepThrough]
get { return this.service; }
}
/// <summary>Return the IUpdatable object to use to make changes to entity states</summary>
protected UpdatableWrapper Updatable
{
get
{
return this.Service.Updatable;
}
}
/// <summary>
/// Returns true if the request method is a PUT (update) method
/// </summary>
protected bool Update
{
[DebuggerStepThrough]
get { return this.update; }
}
/// <summary>Returns the current count of number of objects referred by this request.</summary>
protected int MaxObjectCount
{
get { return this.objectCount; }
}
/// <summary>Request description for the top level target entity.</summary>
protected RequestDescription RequestDescription
{
get { return this.description; }
}
/// <summary>Releases resources held onto by this object.</summary>
void IDisposable.Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Creates a new <see cref="Deserializer"/> for the specified stream.
/// </summary>
/// <param name="description">description about the request uri.</param>
/// <param name="dataService">Data service for which the deserializer will act.</param>
/// <param name="update">indicates whether this is a update operation or not</param>
/// <param name="tracker">Tracker to use for modifications.</param>
/// <returns>A new instance of <see cref="Deserializer"/>.</returns>
internal static Deserializer CreateDeserializer(RequestDescription description, IDataService dataService, bool update, UpdateTracker tracker)
{
string mimeType;
System.Text.Encoding encoding;
DataServiceHostWrapper host = dataService.OperationContext.Host;
HttpProcessUtility.ReadContentType(host.RequestContentType, out mimeType, out encoding);
ContentFormat requestFormat = WebUtil.SelectRequestFormat(mimeType, description);
Stream requestStream = host.RequestStream;
Debug.Assert(requestStream != null, "requestStream != null");
Debug.Assert(tracker != null, "Change tracker must always be created.");
Deserializer deserializer = null;
Debug.Assert(
(!update /*POST*/ && dataService.OperationContext.Host.AstoriaHttpVerb == AstoriaVerbs.POST) ||
(update /*PUT,MERGE*/ && (dataService.OperationContext.Host.AstoriaHttpVerb == AstoriaVerbs.MERGE || dataService.OperationContext.Host.AstoriaHttpVerb == AstoriaVerbs.PUT)),
"For PUT and MERGE, update must be true; for POST, update must be false");
switch (requestFormat)
{
case ContentFormat.Json:
deserializer = new JsonDeserializer(
requestStream,
encoding,
update,
dataService,
tracker);
break;
case ContentFormat.Atom:
SyndicationFormatterFactory factory = new Atom10FormatterFactory();
deserializer = new SyndicationDeserializer(
requestStream, // stream
encoding, // encoding
dataService, // dataService
update,
factory,
tracker); // factory
break;
case ContentFormat.PlainXml:
deserializer = new PlainXmlDeserializer(
requestStream,
encoding,
dataService,
update,
tracker);
break;
default:
throw new DataServiceException(415, Strings.BadRequest_UnsupportedRequestContentType(host.RequestContentType));
}
Debug.Assert(deserializer != null, "deserializer != null");
return deserializer;
}
/// <summary>
/// Converts the given value to the expected type as per the deserializer rules
/// </summary>
/// <param name="value">value to the converted</param>
/// <param name="property">property information whose value is the first parameter</param>
/// <param name="contentFormat">specifies the content format of the payload</param>
/// <param name="provider">underlying data service provider.</param>
/// <returns>object which is in sync with the properties type</returns>
internal static object ConvertValues(object value, ResourceProperty property, ContentFormat contentFormat, DataServiceProviderWrapper provider)
{
Debug.Assert(property.TypeKind == ResourceTypeKind.Primitive, "This method must be called for primitive types only");
if (contentFormat == ContentFormat.Json)
{
return JsonDeserializer.ConvertValues(value, property.Name, property.Type, provider);
}
else if (contentFormat == ContentFormat.Atom || contentFormat == ContentFormat.PlainXml)
{
return PlainXmlDeserializer.ConvertValuesForXml(value, property.Name, property.Type);
}
else
{
Debug.Assert(
contentFormat == ContentFormat.Binary || contentFormat == ContentFormat.Text,
"expecting binary or text");
// Do not do any coversions for them
return value;
}
}
/// <summary>
/// Update the resource specified in the given request description
/// </summary>
/// <param name="description">description about the request uri</param>
/// <param name="dataService">data service type to which the request was made</param>
/// <param name="stream">Stream from which request body should be read.</param>
/// <returns>The tracked modifications.</returns>
internal static RequestDescription HandlePutRequest(RequestDescription description, IDataService dataService, Stream stream)
{
Debug.Assert(stream != null, "stream != null");
Debug.Assert(dataService != null, "dataService != null");
object requestValue = null;
ContentFormat requestFormat;
object entityGettingModified = null;
ResourceSetWrapper container = null;
string mimeType;
Encoding encoding;
DataServiceHostWrapper host = dataService.OperationContext.Host;
HttpProcessUtility.ReadContentType(host.RequestContentType, out mimeType, out encoding);
UpdateTracker tracker = UpdateTracker.CreateUpdateTracker(dataService);
Debug.Assert(tracker != null, "Change tracker must always be created.");
// If its a primitive value that is getting modified, then we need to use the text or binary
// serializer depending on the mime type
if (description.TargetKind == RequestTargetKind.OpenPropertyValue ||
description.TargetKind == RequestTargetKind.PrimitiveValue)
{
string contentType;
requestFormat = WebUtil.GetResponseFormatForPrimitiveValue(description.TargetResourceType, out contentType);
if (!WebUtil.CompareMimeType(contentType, mimeType))
{
if (description.TargetResourceType != null)
{
throw new DataServiceException(415, Strings.BadRequest_InvalidContentType(host.RequestContentType, description.TargetResourceType.Name));
}
else
{
throw new DataServiceException(415, Strings.BadRequest_InvalidContentTypeForOpenProperty(host.RequestContentType, description.ContainerName));
}
}
if (requestFormat == ContentFormat.Binary)
{
byte[] propertyValue = ReadByteStream(stream);
if (description.Property != null && description.Property.Type == typeof(System.Data.Linq.Binary))
{
requestValue = new System.Data.Linq.Binary(propertyValue);
}
else
{
requestValue = propertyValue;
}
}
else
{
Debug.Assert(requestFormat == ContentFormat.Text, "requestFormat == ContentFormat.Text");
Debug.Assert(encoding != null, "encoding != null");
StreamReader requestReader = new StreamReader(stream, encoding);
string propertyValue = Deserializer.ReadStringFromStream(requestReader);
if (description.Property != null && propertyValue != null)
{
try
{
// Convert the property value to the correct type
requestValue = WebConvert.StringToPrimitive((string)propertyValue, description.Property.Type);
}
catch (FormatException e)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ErrorInConvertingPropertyValue(description.Property.Name, description.Property.Type), e);
}
}
else
{
// For open types, there is no conversion required. There is not enough information to do the conversion.
requestValue = propertyValue;
}
}
}
else if (description.TargetKind == RequestTargetKind.MediaResource)
{
requestFormat = ContentFormat.Binary;
requestValue = stream;
}
else
{
requestFormat = WebUtil.SelectRequestFormat(mimeType, description);
using (Deserializer deserializer = Deserializer.CreateDeserializer(description, dataService, true /*update*/, tracker))
{
if (description.LinkUri)
{
string uri = deserializer.GetLinkUriFromPayload();
// No need to check for null - if the uri in the payload is /Customer(1)/BestFriend,
// and the value is null, it means that the user wants to set the current link to null
// i.e. in other words, unbind the relationship.
object linkResource = deserializer.GetTargetResourceToBind(uri, true /*checkNull*/);
entityGettingModified = Deserializer.HandleBindOperation(description, linkResource, deserializer.Service, deserializer.Tracker);
container = description.LastSegmentInfo.TargetContainer;
}
else
{
requestValue = deserializer.ReadEntity(description);
if (requestValue == null &&
description.LastSegmentInfo.HasKeyValues &&
description.TargetSource == RequestTargetSource.EntitySet)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_CannotSetTopLevelResourceToNull(description.ResultUri.OriginalString));
}
}
}
}
// Update the property value, if the request target is property
if (!description.LinkUri && IsQueryRequired(description, requestValue, dataService.Provider))
{
// Get the parent entity and its container and the resource to modify
object resourceToModify = GetResourceToModify(
description, dataService, false /*allowCrossReferencing*/, out entityGettingModified, out container, true /*checkETag*/);
tracker.TrackAction(entityGettingModified, container, UpdateOperations.Change);
Deserializer.ModifyResource(description, resourceToModify, requestValue, requestFormat, dataService);
}
tracker.FireNotifications();
if (entityGettingModified == null)
{
entityGettingModified = requestValue;
container = description.LastSegmentInfo.TargetContainer;
}
return RequestDescription.CreateSingleResultRequestDescription(description, entityGettingModified, container);
}
/// <summary>
/// Gets the resource to modify.
/// </summary>
/// <param name="description">description about the target request</param>
/// <param name="service">data service type to which the request was made</param>
/// <param name="entityResource">entity resource which is getting modified.</param>
/// <param name="container">entity container of the entity which is getting modified.</param>
/// <returns>Returns the object that needs to get modified</returns>
internal static object GetResourceToModify(RequestDescription description, IDataService service, out object entityResource, out ResourceSetWrapper container)
{
return GetResourceToModify(description, service, false /*allowCrossReference*/, out entityResource, out container, false /*checkETag*/);
}
/// <summary>
/// Returns the last segment info whose target request kind is resource
/// </summary>
/// <param name="description">description about the target request</param>
/// <param name="service">data service type to which the request was made</param>
/// <param name="allowCrossReferencing">whether cross-referencing is allowed for the resource in question.</param>
/// <param name="entityResource">entity resource which is getting modified.</param>
/// <param name="entityContainer">entity container of the entity which is getting modified.</param>
/// <param name="checkETag">whether to check the etag for the entity resource that is getting modified.</param>
/// <returns>Returns the object that needs to get modified</returns>
internal static object GetResourceToModify(
RequestDescription description,
IDataService service,
bool allowCrossReferencing,
out object entityResource,
out ResourceSetWrapper entityContainer,
bool checkETag)
{
Debug.Assert(description.SegmentInfos.Length >= 2, "description.SegmentInfos.Length >= 2");
UpdatableWrapper updatable = service.Updatable;
if (!allowCrossReferencing && description.RequestEnumerable == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ResourceCanBeCrossReferencedOnlyForBindOperation);
}
// Set the index of the modifying resource
int modifyingResourceIndex = -1;
if (
description.TargetKind == RequestTargetKind.OpenPropertyValue ||
description.TargetKind == RequestTargetKind.PrimitiveValue)
{
modifyingResourceIndex = description.SegmentInfos.Length - 3;
}
else
{
modifyingResourceIndex = description.SegmentInfos.Length - 2;
}
// Get the index of the entity resource that is getting modified
int entityResourceIndex = -1;
if (description.LinkUri)
{
entityResourceIndex = modifyingResourceIndex;
}
else
{
for (int j = modifyingResourceIndex; j >= 0; j--)
{
if (description.SegmentInfos[j].TargetKind == RequestTargetKind.Resource ||
description.SegmentInfos[j].HasKeyValues)
{
entityResourceIndex = j;
break;
}
}
}
Debug.Assert(entityResourceIndex != -1, "This method should never be called for request that doesn't have a parent resource");
entityContainer = description.SegmentInfos[entityResourceIndex].TargetContainer;
if (entityContainer != null)
{
DataServiceHostWrapper host = service.OperationContext.Host;
// Since this is the entity which is going to get modified, then we need to check for rights
if (host.AstoriaHttpVerb == AstoriaVerbs.PUT)
{
DataServiceConfiguration.CheckResourceRights(entityContainer, EntitySetRights.WriteReplace);
}
else if (host.AstoriaHttpVerb == AstoriaVerbs.MERGE)
{
DataServiceConfiguration.CheckResourceRights(entityContainer, EntitySetRights.WriteMerge);
}
else
{
Debug.Assert(
host.AstoriaHttpVerb == AstoriaVerbs.POST ||
host.AstoriaHttpVerb == AstoriaVerbs.DELETE,
"expecting POST and DELETE methods");
DataServiceConfiguration.CheckResourceRights(entityContainer, EntitySetRights.WriteMerge | EntitySetRights.WriteReplace);
}
}
entityResource = service.GetResource(description, entityResourceIndex, null);
if (entityResource == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_DereferencingNullPropertyValue(description.SegmentInfos[entityResourceIndex].Identifier));
}
// now walk from the entity resource to the resource to modify.
// for open types, as you walk, if the intermediate resource is an entity,
// update the entityResource accordingly.
object resourceToModify = entityResource;
for (int i = entityResourceIndex + 1; i <= modifyingResourceIndex; i++)
{
resourceToModify = updatable.GetValue(resourceToModify, description.SegmentInfos[i].Identifier);
}
if (entityContainer == null)
{
Debug.Assert(
description.TargetKind == RequestTargetKind.OpenProperty ||
description.TargetKind == RequestTargetKind.OpenPropertyValue,
"its a open property target. Hence resource set must be null");
// Open navigation properties are not supported on OpenTypes.
throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(description.SegmentInfos[entityResourceIndex].Identifier));
}
// If checkETag is true, then we need to check the etag for the resource
// Note that MediaResource has a separate etag, we don't need to check the MLE etag if the target kind is MediaResource
if (checkETag && !Deserializer.IsCrossReferencedSegment(description.SegmentInfos[modifyingResourceIndex], service) && description.TargetKind != RequestTargetKind.MediaResource)
{
service.Updatable.SetETagValues(entityResource, entityContainer);
}
return resourceToModify;
}
/// <summary>
/// Modify the value of the given resource to the given value
/// </summary>
/// <param name="description">description about the request</param>
/// <param name="resourceToBeModified">resource that needs to be modified</param>
/// <param name="requestValue">the new value for the target resource</param>
/// <param name="contentFormat">specifies the content format of the payload</param>
/// <param name="service">Service this request is against</param>
internal static void ModifyResource(RequestDescription description, object resourceToBeModified, object requestValue, ContentFormat contentFormat, IDataService service)
{
if (description.TargetKind == RequestTargetKind.OpenProperty ||
description.TargetKind == RequestTargetKind.OpenPropertyValue)
{
Debug.Assert(!description.LastSegmentInfo.HasKeyValues, "CreateSegments must have caught the problem already.");
SetOpenPropertyValue(resourceToBeModified, description.ContainerName, requestValue, service);
}
else if (description.TargetKind == RequestTargetKind.MediaResource)
{
SetStreamPropertyValue(resourceToBeModified, (Stream)requestValue, service, description);
}
else
{
Debug.Assert(
description.TargetKind == RequestTargetKind.Primitive ||
description.TargetKind == RequestTargetKind.ComplexObject ||
description.TargetKind == RequestTargetKind.PrimitiveValue,
"unexpected target kind encountered");
// update the primitive value
ResourceProperty propertyToUpdate = description.LastSegmentInfo.ProjectedProperty;
SetPropertyValue(propertyToUpdate, resourceToBeModified, requestValue, contentFormat, service);
}
}
/// <summary>
/// Get the resource referred by the given segment
/// </summary>
/// <param name="segmentInfo">information about the segment.</param>
/// <param name="fullTypeName">full name of the resource referred by the segment.</param>
/// <param name="service">data service type to which the request was made</param>
/// <param name="checkForNull">whether to check if the resource is null or not.</param>
/// <returns>returns the resource returned by the provider.</returns>
internal static object GetResource(SegmentInfo segmentInfo, string fullTypeName, IDataService service, bool checkForNull)
{
if (segmentInfo.TargetContainer != null)
{
Debug.Assert(
segmentInfo.TargetKind != RequestTargetKind.OpenProperty &&
segmentInfo.TargetKind != RequestTargetKind.OpenPropertyValue,
"container can be null only for open types");
DataServiceConfiguration.CheckResourceRights(segmentInfo.TargetContainer, EntitySetRights.ReadSingle);
}
object resource = service.Updatable.GetResource((IQueryable)segmentInfo.RequestEnumerable, fullTypeName);
if (resource == null &&
(segmentInfo.HasKeyValues || checkForNull))
{
throw DataServiceException.CreateResourceNotFound(segmentInfo.Identifier);
}
return resource;
}
/// <summary>
/// Creates a Media Link Entry.
/// </summary>
/// <param name="fullTypeName">Full type name for the MLE to be created.</param>
/// <param name="requestStream">Request stream from the host.</param>
/// <param name="service">Service this request is against.</param>
/// <param name="description">Description of the target request.</param>
/// <param name="tracker">Update tracker instance to fire change interceptor calls</param>
/// <returns>Newly created Media Link Entry.</returns>
internal static object CreateMediaLinkEntry(string fullTypeName, Stream requestStream, IDataService service, RequestDescription description, UpdateTracker tracker)
{
Debug.Assert(!string.IsNullOrEmpty(fullTypeName), "!string.IsNullOrEmpty(fullTypeName)");
Debug.Assert(requestStream != null, "requestStream != null");
Debug.Assert(service != null, "service != null");
Debug.Assert(description != null, "description != null");
Debug.Assert(tracker != null, "tracker != null");
object entity = service.Updatable.CreateResource(description.LastSegmentInfo.TargetContainer.Name, fullTypeName);
tracker.TrackAction(entity, description.LastSegmentInfo.TargetContainer, UpdateOperations.Add);
SetStreamPropertyValue(entity, requestStream, service, description);
return entity;
}
/// <summary>
/// Copy the contents of the request stream into the default stream of the specified entity.
/// </summary>
/// <param name="resourceToBeModified">Entity with the associated stream which we will write to.</param>
/// <param name="requestStream">Request stream from the host</param>
/// <param name="service">Service this is request is against</param>
/// <param name="description">Description of the target request.</param>
internal static void SetStreamPropertyValue(object resourceToBeModified, Stream requestStream, IDataService service, RequestDescription description)
{
Debug.Assert(resourceToBeModified != null, "resourceToBeModified != null");
Debug.Assert(requestStream.CanRead, "requestStream.CanRead");
Debug.Assert(service != null, "service != null");
resourceToBeModified = service.Updatable.ResolveResource(resourceToBeModified);
ResourceType resourceType = service.Provider.GetResourceType(resourceToBeModified);
if (!resourceType.IsMediaLinkEntry)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_InvalidUriForMediaResource(service.OperationContext.AbsoluteRequestUri));
}
if (service.OperationContext.Host.AstoriaHttpVerb == AstoriaVerbs.MERGE)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.BadRequest_InvalidUriForMergeOperation(service.OperationContext.AbsoluteRequestUri),
DataServiceConfiguration.GetAllowedMethods(service.Configuration, description));
}
using (Stream writeStream = service.StreamProvider.GetWriteStream(resourceToBeModified, service.OperationContext))
{
WebUtil.CopyStream(requestStream, writeStream, service.StreamProvider.StreamBufferSize);
}
}
/// <summary>
/// Gets the resource from the segment enumerable.
/// </summary>
/// <param name="segmentInfo">segment from which resource needs to be returned.</param>
/// <returns>returns the resource contained in the request enumerable.</returns>
internal static object GetCrossReferencedResource(SegmentInfo segmentInfo)
{
Debug.Assert(segmentInfo.RequestEnumerable != null, "The segment should always have the result");
object[] results = (object[])segmentInfo.RequestEnumerable;
Debug.Assert(results != null && results.Length == 1, "results != null && results.Length == 1");
Debug.Assert(results[0] != null, "results[0] != null");
return results[0];
}
/// <summary>
/// Test if the given segment is a cross referenced segment in a batch operation
/// </summary>
/// <param name="segmentInfo">Segment in question</param>
/// <param name="service">service instance</param>
/// <returns>True if the given segment is a cross referenced segment</returns>
internal static bool IsCrossReferencedSegment(SegmentInfo segmentInfo, IDataService service)
{
if (segmentInfo.Identifier.StartsWith("$", StringComparison.Ordinal) && service.GetSegmentForContentId(segmentInfo.Identifier) != null)
{
return true;
}
return false;
}
/// <summary>
/// Handle bind operation
/// </summary>
/// <param name="description">information about the request uri.</param>
/// <param name="linkResource">the child resource which needs to be linked.</param>
/// <param name="service">data service instance</param>
/// <param name="tracker">update tracker instance to fire change interceptor calls</param>
/// <returns>returns the parent object to which an new object was linked to.</returns>
internal static object HandleBindOperation(RequestDescription description, object linkResource, IDataService service, UpdateTracker tracker)
{
Debug.Assert(description != null, "description != null");
Debug.Assert(linkResource != null, "linkResource != null");
Debug.Assert(service != null, "service != null");
Debug.Assert(tracker != null, "tracker != null");
object entityGettingModified;
ResourceSetWrapper container;
object resourceToBeModified = Deserializer.GetResourceToModify(description, service, true /*allowCrossReference*/, out entityGettingModified, out container, false /*checkETag*/);
Debug.Assert(resourceToBeModified == entityGettingModified, "Since this is a link operation, modifying resource must be the entity resource");
// If the container we are modifying contains any type with FF mapped KeepInContent=false properties, we need to raise the FeatureVersion.
description.UpdateAndCheckEpmFeatureVersion(container, service);
tracker.TrackAction(entityGettingModified, container, UpdateOperations.Change);
Debug.Assert(description.Property != null, "description.Property != null");
if (description.IsSingleResult)
{
service.Updatable.SetReference(entityGettingModified, description.Property.Name, linkResource);
}
else
{
service.Updatable.AddReferenceToCollection(entityGettingModified, description.Property.Name, linkResource);
}
return entityGettingModified;
}
/// <summary>
/// Deserializes the given stream into clr object as specified in the payload
/// </summary>
/// <param name="requestDescription">description about the target request</param>
/// <returns>the object instance that it created and populated from the reader</returns>
internal object ReadEntity(RequestDescription requestDescription)
{
Debug.Assert(requestDescription != null, "requestDescription != null");
this.description = requestDescription;
if (requestDescription.TargetKind == RequestTargetKind.Resource)
{
Debug.Assert(requestDescription.LastSegmentInfo != null, "requestDescription.LastSegmentInfo != null");
Debug.Assert(requestDescription.LastSegmentInfo.TargetContainer != null, "requestDescription.LastSegmentInfo.TargetContainer != null");
Debug.Assert(requestDescription.TargetResourceType != null, "requestDescription.TargetResourceType != null");
this.RequestDescription.UpdateAndCheckEpmFeatureVersion(this.description.LastSegmentInfo.TargetContainer, this.Service);
}
// If the description points to a resource,
// we need to materialize the object and return back.
SegmentInfo segmentInfo = requestDescription.LastSegmentInfo;
if (!this.Update)
{
Debug.Assert(!segmentInfo.SingleResult, "POST operation is allowed only on collections");
SegmentInfo adjustedSegment = new SegmentInfo();
adjustedSegment.TargetKind = segmentInfo.TargetKind;
adjustedSegment.TargetSource = segmentInfo.TargetSource;
adjustedSegment.SingleResult = true;
adjustedSegment.ProjectedProperty = segmentInfo.ProjectedProperty;
adjustedSegment.TargetResourceType = segmentInfo.TargetResourceType;
adjustedSegment.TargetContainer = segmentInfo.TargetContainer;
adjustedSegment.Identifier = segmentInfo.Identifier;
segmentInfo = adjustedSegment;
}
return this.CreateSingleObject(segmentInfo);
}
/// <summary>
/// Handles post request.
/// </summary>
/// <param name="requestDescription">description about the uri for the post operation.</param>
/// <returns>returns the resource that is getting inserted or binded - as specified in the payload.</returns>
internal object HandlePostRequest(RequestDescription requestDescription)
{
Debug.Assert(!this.Update, "This method must be called for POST operations only");
Debug.Assert(requestDescription != null, "requestDescription != null");
object resourceInPayload;
if (requestDescription.LinkUri)
{
string uri = this.GetLinkUriFromPayload();
resourceInPayload = this.GetTargetResourceToBind(uri, true /*checkNull*/);
Debug.Assert(resourceInPayload != null, "link resource cannot be null");
Deserializer.HandleBindOperation(requestDescription, resourceInPayload, this.Service, this.Tracker);
}
else
{
if (requestDescription.LastSegmentInfo.TargetContainer != null)
{
DataServiceConfiguration.CheckResourceRights(requestDescription.LastSegmentInfo.TargetContainer, EntitySetRights.WriteAppend);
}
resourceInPayload = this.ReadEntity(requestDescription);
if (requestDescription.TargetSource == RequestTargetSource.Property)
{
Debug.Assert(requestDescription.Property.Kind == ResourcePropertyKind.ResourceSetReference, "Expecting POST resource set property");
Deserializer.HandleBindOperation(requestDescription, resourceInPayload, this.Service, this.Tracker);
}
else
{
Debug.Assert(requestDescription.TargetSource == RequestTargetSource.EntitySet, "Expecting POST on entity set");
this.tracker.TrackAction(resourceInPayload, requestDescription.LastSegmentInfo.TargetContainer, UpdateOperations.Add);
}
}
return resourceInPayload;
}
/// <summary>
/// Update the object count value to the given value.
/// </summary>
/// <param name="value">value to be set for object count.</param>
internal void UpdateObjectCount(int value)
{
Debug.Assert(0 <= value, "MaxObjectCount cannot be initialized to a negative number");
Debug.Assert(value <= this.Service.Configuration.MaxObjectCountOnInsert, "On initialize, the value should be less than max object count");
this.objectCount = value;
}
/// <summary>
/// Set the value of the given resource property to the new value
/// </summary>
/// <param name="resourceProperty">property whose value needs to be updated</param>
/// <param name="declaringResource">instance of the declaring type of the property for which the property value needs to be updated</param>
/// <param name="propertyValue">new value for the property</param>
/// <param name="contentFormat">specifies the content format of the payload</param>
/// <param name="service">Service this is request is against</param>
protected static void SetPropertyValue(ResourceProperty resourceProperty, object declaringResource, object propertyValue, ContentFormat contentFormat, IDataService service)
{
Debug.Assert(
resourceProperty.TypeKind == ResourceTypeKind.ComplexType ||
resourceProperty.TypeKind == ResourceTypeKind.Primitive,
"Only primitive and complex type values must be set via this method");
// For open types, resource property can be null
if (resourceProperty.TypeKind == ResourceTypeKind.Primitive)
{
// Only do the conversion if the provider explicitly asked us to do the configuration.
if (service.Configuration.EnableTypeConversion)
{
// First convert the value of the property to the expected type, as specified in the resource property
propertyValue = ConvertValues(propertyValue, resourceProperty, contentFormat, service.Provider);
}
}
service.Updatable.SetValue(declaringResource, resourceProperty.Name, propertyValue);
}
/// <summary>
/// Set the value of the open property
/// </summary>
/// <param name="declaringResource">instance of the declaring type of the property for which the property value needs to be updated</param>
/// <param name="propertyName">name of the open property to update</param>
/// <param name="propertyValue">new value for the property</param>
/// <param name="service">Service this request is against</param>
protected static void SetOpenPropertyValue(object declaringResource, string propertyName, object propertyValue, IDataService service)
{
service.Updatable.SetValue(declaringResource, propertyName, propertyValue);
}
/// <summary>
/// Reads the content from the stream reader and returns it as string
/// </summary>
/// <param name="streamReader">stream reader from which the content needs to be read</param>
/// <returns>string containing the content as read from the stream reader</returns>
protected static string ReadStringFromStream(StreamReader streamReader)
{
return streamReader.ReadToEnd();
}
/// <summary>
/// Make sure binding operations cannot be performed in PUT operations
/// </summary>
/// <param name="requestVerb">http method name for the request.</param>
protected static void CheckForBindingInPutOperations(AstoriaVerbs requestVerb)
{
// Cannot bind in PUT operations.
if (requestVerb == AstoriaVerbs.PUT)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_CannotUpdateRelatedEntitiesInPut);
}
}
/// <summary>Creates a new SegmentInfo for the specified <paramref name="property"/>.</summary>
/// <param name="property">Property to create segment info for (possibly null).</param>
/// <param name="propertyName">Name for the property.</param>
/// <param name="propertySet">Target resource set for the property.</param>
/// <param name="singleResult">Whether a single result is expected.</param>
/// <returns>
/// A new <see cref="SegmentInfo"/> instance that describes the specfied <paramref name="property"/>
/// as a target, or an open proprty if <paramref name="property"/> is null.
/// </returns>
protected static SegmentInfo CreateSegment(ResourceProperty property, string propertyName, ResourceSetWrapper propertySet, bool singleResult)
{
SegmentInfo result = new SegmentInfo();
result.TargetSource = RequestTargetSource.Property;
result.SingleResult = singleResult;
result.Identifier = propertyName;
if (property == null)
{
result.TargetKind = RequestTargetKind.OpenProperty;
}
else
{
result.TargetKind = RequestTargetKind.Resource;
result.Identifier = propertyName;
result.ProjectedProperty = property;
result.TargetResourceType = property.ResourceType;
result.TargetContainer = propertySet;
}
return result;
}
/// <summary>
/// Create the object from the given payload and return the top level object
/// </summary>
/// <param name="segmentInfo">info about the object being created</param>
/// <returns>instance of the object created</returns>
protected abstract object CreateSingleObject(SegmentInfo segmentInfo);
/// <summary>
/// Get the resource referred by the uri in the payload
/// </summary>
/// <returns>resource referred by the uri in the payload.</returns>
protected abstract string GetLinkUriFromPayload();
/// <summary>Provides an opportunity to clean-up resources.</summary>
/// <param name="disposing">
/// Whether the call is being made from an explicit call to
/// IDisposable.Dispose() rather than through the finalizer.
/// </param>
protected virtual void Dispose(bool disposing)
{
}
/// <summary>Marks the fact that a recursive method was entered, and checks that the depth is allowed.</summary>
protected void RecurseEnter()
{
WebUtil.RecurseEnter(RecursionLimit, ref this.recursionDepth);
}
/// <summary>Marks the fact that a recursive method is leaving.</summary>
protected void RecurseLeave()
{
WebUtil.RecurseLeave(ref this.recursionDepth);
}
/// <summary>
/// Returns the target/child resource to bind to an resource, which might be getting inserted or modified.
/// Since this is a target resource, null is a valid value here (for e.g. /Customers(1)/BestFriend value
/// can be null)
/// </summary>
/// <param name="uri">uri referencing to the resource to be returned.</param>
/// <param name="checkNull">whether the resource can be null or not.</param>
/// <returns>returns the resource as referenced by the uri. Throws 404 if the checkNull is true and the resource returned is null.</returns>
protected object GetTargetResourceToBind(string uri, bool checkNull)
{
//
Uri referencedUri = RequestUriProcessor.GetAbsoluteUriFromReference(uri, this.Service.OperationContext);
RequestDescription requestDescription = RequestUriProcessor.ProcessRequestUri(referencedUri, this.Service);
// Get the resource
object resourceCookie = this.Service.GetResource(requestDescription, requestDescription.SegmentInfos.Length - 1, null);
if (checkNull)
{
WebUtil.CheckResourceExists(resourceCookie != null, requestDescription.LastSegmentInfo.Identifier);
}
return resourceCookie;
}
/// <summary>
/// Gets a resource referenced by the given segment info.
/// </summary>
/// <param name="resourceType">resource type whose instance needs to be created</param>
/// <param name="segmentInfo">segment info containing the description of the uri</param>
/// <param name="verifyETag">verify etag value of the current resource with one specified in the request header</param>
/// <param name="checkForNull">validate that the resource cannot be null.</param>
/// <param name="replaceResource">reset the resource as referred by the segment.</param>
/// <returns>a new instance of the given resource type with key values populated</returns>
protected object GetObjectFromSegmentInfo(
ResourceType resourceType,
SegmentInfo segmentInfo,
bool verifyETag,
bool checkForNull,
bool replaceResource)
{
Debug.Assert(resourceType == null && !verifyETag || resourceType != null, "For etag verification, resource type must be specified");
if (segmentInfo.RequestEnumerable == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ResourceCanBeCrossReferencedOnlyForBindOperation);
}
object resourceCookie;
if (Deserializer.IsCrossReferencedSegment(segmentInfo, this.service))
{
resourceCookie = Deserializer.GetCrossReferencedResource(segmentInfo);
}
else
{
resourceCookie = Deserializer.GetResource(
segmentInfo,
resourceType != null ? resourceType.FullName : null,
this.Service,
checkForNull);
// We only need to check etag if the resource is not cross-referenced. If the resource is cross-referenced,
// there is no good way to checking etag for that resource, since it might have
if (verifyETag)
{
this.service.Updatable.SetETagValues(resourceCookie, segmentInfo.TargetContainer);
}
}
if (replaceResource)
{
Debug.Assert(checkForNull, "For resetting resource, the value cannot be null");
resourceCookie = this.Updatable.ResetResource(resourceCookie);
WebUtil.CheckResourceExists(resourceCookie != null, segmentInfo.Identifier);
}
return resourceCookie;
}
/// <summary>
/// Check and increment the object count
/// </summary>
protected void CheckAndIncrementObjectCount()
{
Debug.Assert(this.Update && this.objectCount == 0 || !this.Update, "For updates, the object count is never tracked");
Debug.Assert(this.objectCount <= this.Service.Configuration.MaxObjectCountOnInsert, "The object count should never exceed the limit");
if (!this.Update)
{
this.objectCount++;
if (this.objectCount > this.Service.Configuration.MaxObjectCountOnInsert)
{
throw new DataServiceException(413, Strings.BadRequest_ExceedsMaxObjectCountOnInsert(this.Service.Configuration.MaxObjectCountOnInsert));
}
}
}
/// <summary>
/// Bump the minimum DSV requirement if the given resource type has friendly feed mappings with KeepInContent=false.
/// Note that the minimum DSV requirement is type (i.e. payload) specific and is applicable for Atom format only.
/// </summary>
/// <param name="resourceType">Resource type to inspect</param>
/// <param name="topLevel">True if resourceType is the type for the top level element in the Atom payload.</param>
protected void UpdateAndCheckEpmRequestResponseDSV(ResourceType resourceType, bool topLevel)
{
Debug.Assert(this.Service != null, "this.Service != null");
Debug.Assert(resourceType != null, "Must have valid resource type");
Debug.Assert(resourceType.ResourceTypeKind == ResourceTypeKind.EntityType, "resourceType.ResourceTypeKind == ResourceTypeKind.EntityType");
Debug.Assert(this.RequestDescription != null, "this.RequestDescription != null");
if (!resourceType.EpmIsV1Compatible)
{
// Only raise the minimum version requirement if this is an Atom request.
if (this is SyndicationDeserializer)
{
this.RequestDescription.RaiseMinimumVersionRequirement(2, 0);
}
// For POST operations we need to raise the response version for Atom if resourceType is the type for the top
// level element since we will serialize the newly created instance of this type in the response payload.
// For PUT/MEREGE we respond with 204 and therefore no backcompat issue with 1.0
if (!this.Update && topLevel)
{
// Friendly feeds must only bump the response version for Atom responses
if (WebUtil.IsAtomMimeType(this.Service.OperationContext.Host.RequestAccept))
{
this.RequestDescription.RaiseResponseVersion(2, 0);
}
}
}
WebUtil.CheckVersion(this.Service, this.RequestDescription);
}
/// <summary>
/// Read the byte from the given input request stream
/// </summary>
/// <param name="stream">input/request stream from which data needs to be read</param>
/// <returns>byte array containing all the data read</returns>
private static byte[] ReadByteStream(Stream stream)
{
byte[] data;
// try to read data from the stream 1k at a time
long numberOfBytesRead = 0;
int result = 0;
List<byte[]> byteData = new List<byte[]>();
do
{
data = new byte[4000];
result = stream.Read(data, 0, data.Length);
numberOfBytesRead += result;
byteData.Add(data);
}
while (result == data.Length);
// Find out the total number of bytes read and copy data from byteData to data
data = new byte[numberOfBytesRead];
for (int i = 0; i < byteData.Count - 1; i++)
{
Buffer.BlockCopy(byteData[i], 0, data, i * 4000, 4000);
}
// For the last thing, copy the remaining number of bytes, not always 4000
Buffer.BlockCopy(byteData[byteData.Count - 1], 0, data, (byteData.Count - 1) * 4000, result);
return data;
}
/// <summary>
/// Returns true if we need to query the provider before updating.
/// </summary>
/// <param name="requestDescription">request description</param>
/// <param name="requestValue">value corresponding to the payload for this request</param>
/// <param name="provider">provider against which the request was targeted</param>
/// <returns>returns true if we need to issue an query to satishfy the request</returns>
private static bool IsQueryRequired(RequestDescription requestDescription, object requestValue, DataServiceProviderWrapper provider)
{
Debug.Assert(requestDescription.IsSingleResult, "requestDescription.IsSingleResult");
if (requestDescription.TargetKind == RequestTargetKind.PrimitiveValue ||
requestDescription.TargetKind == RequestTargetKind.Primitive ||
requestDescription.TargetKind == RequestTargetKind.OpenPropertyValue ||
requestDescription.TargetKind == RequestTargetKind.MediaResource ||
requestDescription.TargetKind == RequestTargetKind.ComplexObject)
{
return true;
}
if (requestDescription.TargetKind == RequestTargetKind.OpenProperty)
{
Debug.Assert(!requestDescription.LastSegmentInfo.HasKeyValues, "CreateSegments must have caught this issue.");
// if the value is null, then just set it, since we don't know the type
if (requestValue == null || WebUtil.IsPrimitiveType(requestValue.GetType()))
{
return true;
}
// otherwise just set the complex type properties
if (WebUtil.GetNonPrimitiveResourceType(provider, requestValue).ResourceTypeKind == ResourceTypeKind.ComplexType)
{
return true;
}
}
return false;
}
}
}
|