using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
namespace Apewer.Web
{
/// Response 模型。
public abstract class ApiModel : IApiModel
{
#region Headers
int _expires = 0;
StringPairs _headers = new StringPairs();
int SafeExpires(int seconds)
{
var s = seconds;
if (s < 0) s = 0;
if (s > 2592000) s = 2592000;
return s;
}
/// 响应缓存的过期时间,以秒为单位。
public virtual int Expires { get => _expires; set => _expires = SafeExpires(value); }
/// 状态。
/// 默认值:200。
public virtual int Status { get; set; }
/// 设置 Response 头。
public virtual StringPairs Headers { get => _headers; set => _headers = value ?? new StringPairs(); }
/// 内容类型。当 Headers 中包含 Content-Type 时此属性将被忽略。
public virtual string ContentType { get; set; }
/// 设置文件名,告知客户端此附件处理此响应。
public virtual string Attachment { get; set; }
#endregion
#region Output
/// 执行输出。
/// 此方法由 API 调用器发起调用,用户程序不应主动调用。
///
public abstract void Output(ApiContext context);
/// 向 HTTP 写入头。
/// 已写入的头。
List WriteHeaders(ApiContext context)
{
var status = Status > 0 ? Status : 200;
if (status != 200) context.Provider.SetStatus(status);
var headers = Headers;
var added = new List(32);
if (headers != null)
{
foreach (var header in headers)
{
if (header.Key.IsEmpty()) continue;
if (header.Value.IsEmpty()) continue;
context.Provider.SetHeader(header.Key, header.Value);
added.Add(header.Key.Lower());
}
}
SetAttachment(context);
context.Provider.SetCache(Expires);
if (!added.Contains("content-type")) context.Provider.SetContentType(ContentType);
return added;
}
/// 在 Response 头中添加用于设置文件名的属性。
void SetAttachment(ApiContext context)
{
var name = Attachment;
if (string.IsNullOrEmpty(name)) return;
var encoded = TextUtility.EncodeUrl(name);
context.Provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}");
}
/// 输出头和响应体,响应体是字节数组。
///
protected void Output(ApiContext context, byte[] bytes)
{
if (context == null) throw new ArgumentNullException(nameof(context));
// 写入头
if (context.Provider == null) return;
if (context.Provider.PreWrite().NotEmpty()) return;
var added = WriteHeaders(context);
// 写入头
var length = bytes == null ? 0 : bytes.Length;
if (!added.Contains("content-length")) context.Provider.SetContentLength(length);
// 写入主体
if (length > 0) context.Provider.ResponseBody().Write(bytes);
// 发送
context.Provider.Sent();
}
/// 以指定参数输出。
///
protected void Output(ApiContext context, Stream stream)
{
if (context == null) throw new ArgumentNullException(nameof(context));
// 写入头
if (context.Provider == null) return;
if (context.Provider.PreWrite().NotEmpty()) return;
var added = WriteHeaders(context);
if (stream == null)
{
context.Provider.SetContentLength(0);
context.Provider.Sent();
}
else
{
// 写入头
if (!added.Contains("content-length"))
{
var length = stream.Length - stream.Position;
context.Provider.SetContentLength(length);
}
// 写入主体
context.Provider.ResponseBody().Write(stream);
// 发送
context.Provider.Sent();
}
}
#endregion
/// 创建对象实例,并设置默认属性。
public ApiModel()
{
Status = 200;
ContentType = "application/octet-stream";
Expires = 0;
Attachment = null;
Headers = new StringPairs();
}
}
/// 输出二进制的 Response 模型。
public class ApiBytesModel : ApiModel
{
/// 向 Body 写入的字节数组。
public byte[] Bytes { get; set; }
/// 输出字节数组。
public override void Output(ApiContext context) => Output(context, Bytes);
/// 创建对象实例,并设置默认属性。
public ApiBytesModel(byte[] bytes = null, string contentType = "application/octet-stream")
{
Bytes = bytes;
ContentType = contentType;
}
}
/// 输出二进制的 Response 模型。
public class ApiStreamModel : ApiModel, IDisposable
{
/// 将要读取的流,用于向 Body 写入。
public Stream Stream { get; set; }
/// 执行输出后释放流。
/// 默认值:TRUE。
public bool AutoDispose { get; set; }
/// 输出流。
public override void Output(ApiContext context)
{
if (AutoDispose)
{
using (var stream = Stream) Output(context, Stream);
}
else
{
Output(context, Stream);
}
}
/// 当指定 AutoDispose 属性时释放流。
public void Dispose()
{
if (AutoDispose) RuntimeUtility.Dispose(Stream);
}
/// 创建对象实例,并设置默认属性。
public ApiStreamModel(Stream stream = null, bool autoDispose = true)
{
Stream = stream;
AutoDispose = autoDispose;
}
}
/// 输出二进制的 Response 模型。
public class ApiFileModel : ApiModel
{
string _path;
void SetPath(string path)
{
if (string.IsNullOrEmpty(path)) throw new FileNotFoundException("没有指定文件路径。");
if (!File.Exists(path)) throw new FileNotFoundException($"文件 {path} 不存在。");
_path = path;
}
/// 将要读取的文件所在路径,用于向 Body 写入。
///
public string Path { get => _path; set => SetPath(value); }
/// 输出指定路径的文件。
public override void Output(ApiContext context)
{
if (!File.Exists(Path)) return;
var info = new FileInfo(Path);
if (string.IsNullOrEmpty(Attachment)) Attachment = info.Name;
using (var stream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Output(context, stream);
}
}
///
///
public ApiFileModel(string path)
{
SetPath(path);
}
}
/// 输出文本的 Response 模型。
public class ApiTextModel : ApiModel
{
/// 自定义文本。
public string Text { get; set; }
/// 输出文本。
public override void Output(ApiContext context) => Output(context, TextUtility.Bytes(Text));
/// 创建对象实例,并设置默认属性。
public ApiTextModel(string text = null, string contentType = "text/plain")
{
ContentType = contentType;
Text = text;
}
}
/// 输出文本的 Response 模型。
public class ApiJsonModel : ApiModel
{
/// Json 对象。
public Json Json { get; set; }
/// 缩进排版。
/// 默认值:TRUE
public bool Indented { get; set; }
/// 转为属性名为驼峰形式。
/// 默认值:FALSE
public bool Camel { get; set; }
/// 输出文本。
public override void Output(ApiContext context)
{
var json = (Json != null && Json.Available) ? Json : Json.NewObject();
if (Camel) Json.Camel(json);
Output(context, TextUtility.Bytes(json.ToString(Indented)));
}
/// 创建对象实例,并设置默认属性。
public ApiJsonModel(Json json = null, bool indented = false, bool camel = true)
{
ContentType = "application/json";
Camel = camel;
Indented = indented;
Json = json;
}
}
/// 输出重定向的 Response 模型。
public class ApiRedirectModel : ApiModel
{
/// 将要重定向的位置。
public string Location { get; private set; }
/// 执行重定向。
public override void Output(ApiContext context)
{
var location = Location;
if (string.IsNullOrEmpty(location)) return;
context.Provider.SetRedirect(Location);
}
/// 重定向到指定的 URL。
///
public ApiRedirectModel(string location)
{
if (location.IsEmpty()) throw new ArgumentNullException(nameof(location));
Location = location;
}
}
/// 输出说明 Exception 的 Response 模型。
public class ApiExceptionModel : ApiModel
{
/// 要输出的 Exception。
public Exception Exception { get; set; }
/// 解析 Exception 的内容并输出。
public override void Output(ApiContext context)
{
Status = 500;
ContentType = "text/plain";
Output(context, Format(Exception).Bytes());
}
///
///
public ApiExceptionModel(Exception exception)
{
if (exception == null) throw new ArgumentNullException(nameof(exception));
Exception = exception;
}
///
static string Format(Exception ex)
{
var sb = new StringBuilder();
if (ex == null)
{
sb.Append("Invalid Exception");
}
else
{
try
{
sb.Append(ex.GetType().FullName);
var props = ex.GetType().GetProperties();
foreach (var prop in props)
{
var getter = prop.GetGetMethod();
if (getter == null) continue;
var value = getter.Invoke(ex, null);
if (value == null) continue;
sb.Append("\r\n\r\n");
sb.Append(prop.Name);
sb.Append(" : ");
sb.Append(prop.PropertyType.FullName);
sb.Append("\r\n");
if (value is Json) sb.Append(((Json)value).ToString(true));
else sb.Append(value.ToString() ?? "");
}
// sb.Append("\r\n\r\nToString\r\n");
// sb.Append("\r\n");
// sb.Append(ex.ToString());
}
catch { }
}
sb.Append("\r\n");
var text = sb.ToString();
return text;
}
}
/// 输出带有指定 Status 的 Response 模型。
public class ApiStatusModel : ApiModel
{
/// 向 Body 写入的字节数组。
public byte[] Bytes { get; set; }
/// 执行重定向。
public override void Output(ApiContext context) => Output(context, Bytes);
///
public ApiStatusModel(int status = 200) => Status = status;
///
public ApiStatusModel(HttpStatusCode status) => Status = (int)status;
}
}