// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ApiExplorer; using Microsoft.AspNet.Mvc.Core; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Mvc { /// /// Writes an object to the output stream. /// public abstract class OutputFormatter : IOutputFormatter, IApiResponseFormatMetadataProvider { // using a field so we can return it as both IList and IReadOnlyList private readonly List _supportedMediaTypes; /// /// Initializes a new instance of the class. /// protected OutputFormatter() { SupportedEncodings = new List(); _supportedMediaTypes = new List(); } /// /// Gets the mutable collection of character encodings supported by /// this . The encodings are /// used when writing the data. /// public IList SupportedEncodings { get; } /// /// Gets the mutable collection of elements supported by /// this . /// public IList SupportedMediaTypes { get { return _supportedMediaTypes; } } /// /// Returns a value indicating whether or not the given type can be written by this serializer. /// /// The declared type. /// The runtime type. /// true if the type can be written, otherwise false. protected virtual bool CanWriteType(Type declaredType, Type runtimeType) { return true; } /// public virtual IReadOnlyList GetSupportedContentTypes( Type declaredType, Type runtimeType, MediaTypeHeaderValue contentType) { if (!CanWriteType(declaredType, runtimeType)) { return null; } if (contentType == null) { // If contentType is null, then any type we support is valid. return _supportedMediaTypes.Count > 0 ? _supportedMediaTypes : null; } else { List mediaTypes = null; foreach (var mediaType in _supportedMediaTypes) { if (mediaType.IsSubsetOf(contentType)) { if (mediaTypes == null) { mediaTypes = new List(); } mediaTypes.Add(mediaType); } } return mediaTypes; } } /// /// Determines the best amongst the supported encodings /// for reading or writing an HTTP entity body based on the provided . /// /// The formatter context associated with the call. /// /// The to use when reading the request or writing the response. public virtual Encoding SelectCharacterEncoding([NotNull] OutputFormatterContext context) { var request = context.HttpContext.Request; var encoding = MatchAcceptCharacterEncoding(request.GetTypedHeaders().AcceptCharset); if (encoding == null) { // Match based on request acceptHeader. MediaTypeHeaderValue requestContentType = null; if (MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType) && !string.IsNullOrEmpty(requestContentType.Charset)) { var requestCharset = requestContentType.Charset; encoding = SupportedEncodings.FirstOrDefault( supportedEncoding => requestCharset.Equals(supportedEncoding.WebName)); } } encoding = encoding ?? SupportedEncodings.FirstOrDefault(); return encoding; } /// public virtual bool CanWriteResult([NotNull] OutputFormatterContext context, MediaTypeHeaderValue contentType) { var runtimeType = context.Object == null ? null : context.Object.GetType(); if (!CanWriteType(context.DeclaredType, runtimeType)) { return false; } MediaTypeHeaderValue mediaType = null; if (contentType == null) { // If the desired content type is set to null, the current formatter is free to choose the // response media type. mediaType = SupportedMediaTypes.FirstOrDefault(); } else { // Since supportedMedia Type is going to be more specific check if supportedMediaType is a subset // of the content type which is typically what we get on acceptHeader. mediaType = SupportedMediaTypes .FirstOrDefault(supportedMediaType => supportedMediaType.IsSubsetOf(contentType)); } if (mediaType != null) { context.SelectedContentType = mediaType; return true; } return false; } /// public async Task WriteAsync([NotNull] OutputFormatterContext context) { WriteResponseHeaders(context); await WriteResponseBodyAsync(context); } /// /// Sets the headers on object. /// /// The formatter context associated with the call. public virtual void WriteResponseHeaders([NotNull] OutputFormatterContext context) { var selectedMediaType = context.SelectedContentType; // If content type is not set then set it based on supported media types. selectedMediaType = selectedMediaType ?? SupportedMediaTypes.FirstOrDefault(); if (selectedMediaType == null) { throw new InvalidOperationException(Resources.FormatOutputFormatterNoMediaType(GetType().FullName)); } // Copy the media type as we don't want it to affect the next request selectedMediaType = selectedMediaType.Copy(); // Not text-based media types will use an encoding/charset - binary formats just ignore it. We want to // make this class work with media types that use encodings, and those that don't. // // The default implementation of SelectCharacterEncoding will read from the list of SupportedEncodings // and will always choose a default encoding if any are supported. So, the only cases where the // selectedEncoding can be null are: // // 1). No supported encodings - we assume this is a non-text format // 2). Custom implementation of SelectCharacterEncoding - trust the user and give them what they want. var selectedEncoding = SelectCharacterEncoding(context); if (selectedEncoding != null) { context.SelectedEncoding = selectedEncoding; // Override the content type value even if one already existed. selectedMediaType.Charset = selectedEncoding.WebName; } context.SelectedContentType = context.SelectedContentType ?? selectedMediaType; var response = context.HttpContext.Response; response.ContentType = selectedMediaType.ToString(); } /// /// Writes the response body. /// /// The formatter context associated with the call. /// A task which can write the response body. public abstract Task WriteResponseBodyAsync([NotNull] OutputFormatterContext context); private Encoding MatchAcceptCharacterEncoding(IList acceptCharsetHeaders) { if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0) { var sortedAcceptCharsetHeaders = acceptCharsetHeaders .Where(acceptCharset => acceptCharset.Quality != HeaderQuality.NoMatch) .OrderByDescending( m => m, StringWithQualityHeaderValueComparer.QualityComparer); foreach (var acceptCharset in sortedAcceptCharsetHeaders) { var charset = acceptCharset.Value; if (!string.IsNullOrWhiteSpace(charset)) { var encoding = SupportedEncodings.FirstOrDefault( supportedEncoding => charset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase) || charset.Equals("*", StringComparison.Ordinal)); if (encoding != null) { return encoding; } } } } return null; } } }