diff options
Diffstat (limited to 'ServiceStack')
25 files changed, 2755 insertions, 0 deletions
diff --git a/ServiceStack/FilterAttributeCache.cs b/ServiceStack/FilterAttributeCache.cs new file mode 100644 index 000000000..378433add --- /dev/null +++ b/ServiceStack/FilterAttributeCache.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using ServiceStack; + +namespace ServiceStack.Support.WebHost +{ + public static class FilterAttributeCache + { + public static MediaBrowser.Model.Services.IHasRequestFilter[] GetRequestFilterAttributes(Type requestDtoType) + { + var attributes = requestDtoType.AllAttributes().OfType<MediaBrowser.Model.Services.IHasRequestFilter>().ToList(); + + var serviceType = ServiceStackHost.Instance.Metadata.GetServiceTypeByRequest(requestDtoType); + if (serviceType != null) + { + attributes.AddRange(serviceType.AllAttributes().OfType<MediaBrowser.Model.Services.IHasRequestFilter>()); + } + + attributes.Sort((x,y) => x.Priority - y.Priority); + + return attributes.ToArray(); + } + } +} diff --git a/ServiceStack/Host/ActionContext.cs b/ServiceStack/Host/ActionContext.cs new file mode 100644 index 000000000..9f165cff1 --- /dev/null +++ b/ServiceStack/Host/ActionContext.cs @@ -0,0 +1,27 @@ +using System; + +namespace ServiceStack.Host +{ + /// <summary> + /// Context to capture IService action + /// </summary> + public class ActionContext + { + public const string AnyAction = "ANY"; + + public string Id { get; set; } + + public ActionInvokerFn ServiceAction { get; set; } + public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; } + + public static string Key(Type serviceType, string method, string requestDtoName) + { + return serviceType.FullName + " " + method.ToUpper() + " " + requestDtoName; + } + + public static string AnyKey(Type serviceType, string requestDtoName) + { + return Key(serviceType, AnyAction, requestDtoName); + } + } +}
\ No newline at end of file diff --git a/ServiceStack/Host/ContentTypes.cs b/ServiceStack/Host/ContentTypes.cs new file mode 100644 index 000000000..22fdc3e50 --- /dev/null +++ b/ServiceStack/Host/ContentTypes.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public class ContentTypes + { + public static ContentTypes Instance = new ContentTypes(); + + public void SerializeToStream(IRequest req, object response, Stream responseStream) + { + var contentType = req.ResponseContentType; + var serializer = GetResponseSerializer(contentType); + if (serializer == null) + throw new NotSupportedException("ContentType not supported: " + contentType); + + var httpRes = new HttpResponseStreamWrapper(responseStream, req) + { + Dto = req.Response.Dto + }; + serializer(req, response, httpRes); + } + + public Action<IRequest, object, IResponse> GetResponseSerializer(string contentType) + { + var serializer = GetStreamSerializer(contentType); + if (serializer == null) return null; + + return (httpReq, dto, httpRes) => serializer(httpReq, dto, httpRes.OutputStream); + } + + public Action<IRequest, object, Stream> GetStreamSerializer(string contentType) + { + switch (GetRealContentType(contentType)) + { + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return (r, o, s) => ServiceStackHost.Instance.SerializeToXml(o, s); + + case "application/json": + case "text/json": + return (r, o, s) => ServiceStackHost.Instance.SerializeToJson(o, s); + } + + return null; + } + + public Func<Type, Stream, object> GetStreamDeserializer(string contentType) + { + switch (GetRealContentType(contentType)) + { + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return ServiceStackHost.Instance.DeserializeXml; + + case "application/json": + case "text/json": + return ServiceStackHost.Instance.DeserializeJson; + } + + return null; + } + + private static string GetRealContentType(string contentType) + { + return contentType == null + ? null + : contentType.Split(';')[0].ToLower().Trim(); + } + + } +}
\ No newline at end of file diff --git a/ServiceStack/Host/HttpResponseStreamWrapper.cs b/ServiceStack/Host/HttpResponseStreamWrapper.cs new file mode 100644 index 000000000..33038da72 --- /dev/null +++ b/ServiceStack/Host/HttpResponseStreamWrapper.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public class HttpResponseStreamWrapper : IHttpResponse + { + private static readonly UTF8Encoding UTF8EncodingWithoutBom = new UTF8Encoding(false); + + public HttpResponseStreamWrapper(Stream stream, IRequest request) + { + this.OutputStream = stream; + this.Request = request; + this.Headers = new Dictionary<string, string>(); + this.Items = new Dictionary<string, object>(); + } + + public Dictionary<string, string> Headers { get; set; } + + public object OriginalResponse + { + get { return null; } + } + + public IRequest Request { get; private set; } + + public int StatusCode { set; get; } + public string StatusDescription { set; get; } + public string ContentType { get; set; } + + public void AddHeader(string name, string value) + { + this.Headers[name] = value; + } + + public string GetHeader(string name) + { + return this.Headers[name]; + } + + public void Redirect(string url) + { + this.Headers["Location"] = url; + } + + public Stream OutputStream { get; private set; } + + public object Dto { get; set; } + + public void Write(string text) + { + var bytes = UTF8EncodingWithoutBom.GetBytes(text); + OutputStream.Write(bytes, 0, bytes.Length); + } + + public bool UseBufferedStream { get; set; } + + public void Close() + { + if (IsClosed) return; + + OutputStream.Dispose(); + IsClosed = true; + } + + public void End() + { + Close(); + } + + public void Flush() + { + OutputStream.Flush(); + } + + public bool IsClosed { get; private set; } + + public void SetContentLength(long contentLength) {} + + public bool KeepAlive { get; set; } + + public Dictionary<string, object> Items { get; private set; } + + public void SetCookie(Cookie cookie) + { + } + + public void ClearCookies() + { + } + } +}
\ No newline at end of file diff --git a/ServiceStack/Host/RestHandler.cs b/ServiceStack/Host/RestHandler.cs new file mode 100644 index 000000000..5c360d150 --- /dev/null +++ b/ServiceStack/Host/RestHandler.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public class RestHandler + { + public string RequestName { get; set; } + + public async Task<object> HandleResponseAsync(object response) + { + var taskResponse = response as Task; + + if (taskResponse == null) + { + return response; + } + + await taskResponse.ConfigureAwait(false); + + var taskResult = ServiceStackHost.Instance.GetTaskResult(taskResponse, RequestName); + + var taskResults = taskResult as Task[]; + + if (taskResults == null) + { + var subTask = taskResult as Task; + if (subTask != null) + taskResult = ServiceStackHost.Instance.GetTaskResult(subTask, RequestName); + + return taskResult; + } + + if (taskResults.Length == 0) + { + return new object[] { }; + } + + var firstResponse = ServiceStackHost.Instance.GetTaskResult(taskResults[0], RequestName); + var batchedResponses = firstResponse != null + ? (object[])Array.CreateInstance(firstResponse.GetType(), taskResults.Length) + : new object[taskResults.Length]; + batchedResponses[0] = firstResponse; + for (var i = 1; i < taskResults.Length; i++) + { + batchedResponses[i] = ServiceStackHost.Instance.GetTaskResult(taskResults[i], RequestName); + } + return batchedResponses; + } + + protected static object CreateContentTypeRequest(IRequest httpReq, Type requestType, string contentType) + { + if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) + { + var deserializer = ContentTypes.Instance.GetStreamDeserializer(contentType); + if (deserializer != null) + { + return deserializer(requestType, httpReq.InputStream); + } + } + return ServiceStackHost.Instance.CreateInstance(requestType); //Return an empty DTO, even for empty request bodies + } + + protected static object GetCustomRequestFromBinder(IRequest httpReq, Type requestType) + { + Func<IRequest, object> requestFactoryFn; + ServiceStackHost.Instance.ServiceController.RequestTypeFactoryMap.TryGetValue( + requestType, out requestFactoryFn); + + return requestFactoryFn != null ? requestFactoryFn(httpReq) : null; + } + + public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) + { + pathInfo = GetSanitizedPathInfo(pathInfo, out contentType); + + return ServiceStackHost.Instance.ServiceController.GetRestPathForRequest(httpMethod, pathInfo); + } + + public static string GetSanitizedPathInfo(string pathInfo, out string contentType) + { + contentType = null; + var pos = pathInfo.LastIndexOf('.'); + if (pos >= 0) + { + var format = pathInfo.Substring(pos + 1); + contentType = GetFormatContentType(format); + if (contentType != null) + { + pathInfo = pathInfo.Substring(0, pos); + } + } + return pathInfo; + } + + private static string GetFormatContentType(string format) + { + //built-in formats + if (format == "json") + return "application/json"; + if (format == "xml") + return "application/xml"; + + return null; + } + + public RestPath GetRestPath(string httpMethod, string pathInfo) + { + if (this.RestPath == null) + { + string contentType; + this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out contentType); + + if (contentType != null) + ResponseContentType = contentType; + } + return this.RestPath; + } + + public RestPath RestPath { get; set; } + + // Set from SSHHF.GetHandlerForPathInfo() + public string ResponseContentType { get; set; } + + public async Task ProcessRequestAsync(IRequest httpReq, IResponse httpRes, string operationName) + { + var appHost = ServiceStackHost.Instance; + + var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo); + if (restPath == null) + { + throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo); + } + httpReq.SetRoute(restPath); + + if (ResponseContentType != null) + httpReq.ResponseContentType = ResponseContentType; + + var request = httpReq.Dto = CreateRequest(httpReq, restPath); + + if (appHost.ApplyRequestFilters(httpReq, httpRes, request)) + return; + + var rawResponse = await ServiceStackHost.Instance.ServiceController.Execute(request, httpReq).ConfigureAwait(false); + + if (httpRes.IsClosed) + return; + + var response = await HandleResponseAsync(rawResponse).ConfigureAwait(false); + + if (appHost.ApplyResponseFilters(httpReq, httpRes, response)) + return; + + await httpRes.WriteToResponse(httpReq, response).ConfigureAwait(false); + } + + public static object CreateRequest(IRequest httpReq, RestPath restPath) + { + var dtoFromBinder = GetCustomRequestFromBinder(httpReq, restPath.RequestType); + if (dtoFromBinder != null) + return dtoFromBinder; + + var requestParams = httpReq.GetFlattenedRequestParams(); + return CreateRequest(httpReq, restPath, requestParams); + } + + public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams) + { + var requestDto = CreateContentTypeRequest(httpReq, restPath.RequestType, httpReq.ContentType); + + return CreateRequest(httpReq, restPath, requestParams, requestDto); + } + + public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto) + { + string contentType; + var pathInfo = !restPath.IsWildCardPath + ? GetSanitizedPathInfo(httpReq.PathInfo, out contentType) + : httpReq.PathInfo; + + return restPath.CreateRequest(pathInfo, requestParams, requestDto); + } + + /// <summary> + /// Used in Unit tests + /// </summary> + /// <returns></returns> + public object CreateRequest(IRequest httpReq, string operationName) + { + if (this.RestPath == null) + throw new ArgumentNullException("No RestPath found"); + + return CreateRequest(httpReq, this.RestPath); + } + } + +} diff --git a/ServiceStack/Host/RestPath.cs b/ServiceStack/Host/RestPath.cs new file mode 100644 index 000000000..7222578a9 --- /dev/null +++ b/ServiceStack/Host/RestPath.cs @@ -0,0 +1,443 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using ServiceStack.Serialization; + +namespace ServiceStack.Host +{ + public class RestPath + { + private const string WildCard = "*"; + private const char WildCardChar = '*'; + private const string PathSeperator = "/"; + private const char PathSeperatorChar = '/'; + private const char ComponentSeperator = '.'; + private const string VariablePrefix = "{"; + + readonly bool[] componentsWithSeparators; + + private readonly string restPath; + private readonly string allowedVerbs; + private readonly bool allowsAllVerbs; + public bool IsWildCardPath { get; private set; } + + private readonly string[] literalsToMatch; + + private readonly string[] variablesNames; + + private readonly bool[] isWildcard; + private readonly int wildcardCount = 0; + + public int VariableArgsCount { get; set; } + + /// <summary> + /// The number of segments separated by '/' determinable by path.Split('/').Length + /// e.g. /path/to/here.ext == 3 + /// </summary> + public int PathComponentsCount { get; set; } + + /// <summary> + /// The total number of segments after subparts have been exploded ('.') + /// e.g. /path/to/here.ext == 4 + /// </summary> + public int TotalComponentsCount { get; set; } + + public string[] Verbs + { + get + { + return allowsAllVerbs + ? new[] { ActionContext.AnyAction } + : AllowedVerbs.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + } + } + + public Type RequestType { get; private set; } + + public string Path { get { return this.restPath; } } + + public string Summary { get; private set; } + + public string Notes { get; private set; } + + public bool AllowsAllVerbs { get { return this.allowsAllVerbs; } } + + public string AllowedVerbs { get { return this.allowedVerbs; } } + + public int Priority { get; set; } //passed back to RouteAttribute + + public static string[] GetPathPartsForMatching(string pathInfo) + { + var parts = pathInfo.ToLower().Split(PathSeperatorChar) + .Where(x => !string.IsNullOrEmpty(x)).ToArray(); + return parts; + } + + public static IEnumerable<string> GetFirstMatchHashKeys(string[] pathPartsForMatching) + { + var hashPrefix = pathPartsForMatching.Length + PathSeperator; + return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); + } + + public static IEnumerable<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching) + { + const string hashPrefix = WildCard + PathSeperator; + return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); + } + + private static IEnumerable<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching) + { + foreach (var part in pathPartsForMatching) + { + yield return hashPrefix + part; + var subParts = part.Split(ComponentSeperator); + if (subParts.Length == 1) continue; + + foreach (var subPart in subParts) + { + yield return hashPrefix + subPart; + } + } + } + + public RestPath(Type requestType, string path, string verbs, string summary = null, string notes = null) + { + this.RequestType = requestType; + this.Summary = summary; + this.Notes = notes; + this.restPath = path; + + this.allowsAllVerbs = verbs == null || verbs == WildCard; + if (!this.allowsAllVerbs) + { + this.allowedVerbs = verbs.ToUpper(); + } + + var componentsList = new List<string>(); + + //We only split on '.' if the restPath has them. Allows for /{action}.{type} + var hasSeparators = new List<bool>(); + foreach (var component in this.restPath.Split(PathSeperatorChar)) + { + if (string.IsNullOrEmpty(component)) continue; + + if (component.Contains(VariablePrefix) + && component.IndexOf(ComponentSeperator) != -1) + { + hasSeparators.Add(true); + componentsList.AddRange(component.Split(ComponentSeperator)); + } + else + { + hasSeparators.Add(false); + componentsList.Add(component); + } + } + + var components = componentsList.ToArray(); + this.TotalComponentsCount = components.Length; + + this.literalsToMatch = new string[this.TotalComponentsCount]; + this.variablesNames = new string[this.TotalComponentsCount]; + this.isWildcard = new bool[this.TotalComponentsCount]; + this.componentsWithSeparators = hasSeparators.ToArray(); + this.PathComponentsCount = this.componentsWithSeparators.Length; + string firstLiteralMatch = null; + + var sbHashKey = new StringBuilder(); + for (var i = 0; i < components.Length; i++) + { + var component = components[i]; + + if (component.StartsWith(VariablePrefix)) + { + var variableName = component.Substring(1, component.Length - 2); + if (variableName[variableName.Length - 1] == WildCardChar) + { + this.isWildcard[i] = true; + variableName = variableName.Substring(0, variableName.Length - 1); + } + this.variablesNames[i] = variableName; + this.VariableArgsCount++; + } + else + { + this.literalsToMatch[i] = component.ToLower(); + sbHashKey.Append(i + PathSeperatorChar.ToString() + this.literalsToMatch); + + if (firstLiteralMatch == null) + { + firstLiteralMatch = this.literalsToMatch[i]; + } + } + } + + for (var i = 0; i < components.Length - 1; i++) + { + if (!this.isWildcard[i]) continue; + if (this.literalsToMatch[i + 1] == null) + { + throw new ArgumentException( + "A wildcard path component must be at the end of the path or followed by a literal path component."); + } + } + + this.wildcardCount = this.isWildcard.Count(x => x); + this.IsWildCardPath = this.wildcardCount > 0; + + this.FirstMatchHashKey = !this.IsWildCardPath + ? this.PathComponentsCount + PathSeperator + firstLiteralMatch + : WildCardChar + PathSeperator + firstLiteralMatch; + + this.IsValid = sbHashKey.Length > 0; + this.UniqueMatchHashKey = sbHashKey.ToString(); + + this.typeDeserializer = new StringMapTypeDeserializer(this.RequestType); + RegisterCaseInsenstivePropertyNameMappings(); + } + + private void RegisterCaseInsenstivePropertyNameMappings() + { + foreach (var propertyInfo in RequestType.GetSerializableProperties()) + { + var propertyName = propertyInfo.Name; + propertyNamesMap.Add(propertyName.ToLower(), propertyName); + } + } + + public bool IsValid { get; set; } + + /// <summary> + /// Provide for quick lookups based on hashes that can be determined from a request url + /// </summary> + public string FirstMatchHashKey { get; private set; } + + public string UniqueMatchHashKey { get; private set; } + + private readonly StringMapTypeDeserializer typeDeserializer; + + private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>(); + + public static Func<RestPath, string, string[], int> CalculateMatchScore { get; set; } + + public int MatchScore(string httpMethod, string[] withPathInfoParts) + { + if (CalculateMatchScore != null) + return CalculateMatchScore(this, httpMethod, withPathInfoParts); + + int wildcardMatchCount; + var isMatch = IsMatch(httpMethod, withPathInfoParts, out wildcardMatchCount); + if (!isMatch) return -1; + + var score = 0; + + //Routes with least wildcard matches get the highest score + score += Math.Max((100 - wildcardMatchCount), 1) * 1000; + + //Routes with less variable (and more literal) matches + score += Math.Max((10 - VariableArgsCount), 1) * 100; + + //Exact verb match is better than ANY + var exactVerb = httpMethod == AllowedVerbs; + score += exactVerb ? 10 : 1; + + return score; + } + + /// <summary> + /// For performance withPathInfoParts should already be a lower case string + /// to minimize redundant matching operations. + /// </summary> + /// <param name="httpMethod"></param> + /// <param name="withPathInfoParts"></param> + /// <param name="wildcardMatchCount"></param> + /// <returns></returns> + public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount) + { + wildcardMatchCount = 0; + + if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) return false; + if (!this.allowsAllVerbs && !this.allowedVerbs.Contains(httpMethod.ToUpper())) return false; + + if (!ExplodeComponents(ref withPathInfoParts)) return false; + if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) return false; + + int pathIx = 0; + for (var i = 0; i < this.TotalComponentsCount; i++) + { + if (this.isWildcard[i]) + { + if (i < this.TotalComponentsCount - 1) + { + // Continue to consume up until a match with the next literal + while (pathIx < withPathInfoParts.Length && withPathInfoParts[pathIx] != this.literalsToMatch[i + 1]) + { + pathIx++; + wildcardMatchCount++; + } + + // Ensure there are still enough parts left to match the remainder + if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1)) + { + return false; + } + } + else + { + // A wildcard at the end matches the remainder of path + wildcardMatchCount += withPathInfoParts.Length - pathIx; + pathIx = withPathInfoParts.Length; + } + } + else + { + var literalToMatch = this.literalsToMatch[i]; + if (literalToMatch == null) + { + // Matching an ordinary (non-wildcard) variable consumes a single part + pathIx++; + continue; + } + + if (withPathInfoParts.Length <= pathIx || withPathInfoParts[pathIx] != literalToMatch) return false; + pathIx++; + } + } + + return pathIx == withPathInfoParts.Length; + } + + private bool ExplodeComponents(ref string[] withPathInfoParts) + { + var totalComponents = new List<string>(); + for (var i = 0; i < withPathInfoParts.Length; i++) + { + var component = withPathInfoParts[i]; + if (string.IsNullOrEmpty(component)) continue; + + if (this.PathComponentsCount != this.TotalComponentsCount + && this.componentsWithSeparators[i]) + { + var subComponents = component.Split(ComponentSeperator); + if (subComponents.Length < 2) return false; + totalComponents.AddRange(subComponents); + } + else + { + totalComponents.Add(component); + } + } + + withPathInfoParts = totalComponents.ToArray(); + return true; + } + + public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance) + { + var requestComponents = pathInfo.Split(PathSeperatorChar) + .Where(x => !string.IsNullOrEmpty(x)).ToArray(); + + ExplodeComponents(ref requestComponents); + + if (requestComponents.Length != this.TotalComponentsCount) + { + var isValidWildCardPath = this.IsWildCardPath + && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount; + + if (!isValidWildCardPath) + throw new ArgumentException(string.Format( + "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'", + pathInfo, this.restPath)); + } + + var requestKeyValuesMap = new Dictionary<string, string>(); + var pathIx = 0; + for (var i = 0; i < this.TotalComponentsCount; i++) + { + var variableName = this.variablesNames[i]; + if (variableName == null) + { + pathIx++; + continue; + } + + string propertyNameOnRequest; + if (!this.propertyNamesMap.TryGetValue(variableName.ToLower(), out propertyNameOnRequest)) + { + if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) + { + pathIx++; + continue; + } + + throw new ArgumentException("Could not find property " + + variableName + " on " + RequestType.GetOperationName()); + } + + var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; //wildcard has arg mismatch + if (value != null && this.isWildcard[i]) + { + if (i == this.TotalComponentsCount - 1) + { + // Wildcard at end of path definition consumes all the rest + var sb = new StringBuilder(); + sb.Append(value); + for (var j = pathIx + 1; j < requestComponents.Length; j++) + { + sb.Append(PathSeperatorChar + requestComponents[j]); + } + value = sb.ToString(); + } + else + { + // Wildcard in middle of path definition consumes up until it + // hits a match for the next element in the definition (which must be a literal) + // It may consume 0 or more path parts + var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; + if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) + { + var sb = new StringBuilder(); + sb.Append(value); + pathIx++; + while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) + { + sb.Append(PathSeperatorChar + requestComponents[pathIx++]); + } + value = sb.ToString(); + } + else + { + value = null; + } + } + } + else + { + // Variable consumes single path item + pathIx++; + } + + requestKeyValuesMap[propertyNameOnRequest] = value; + } + + if (queryStringAndFormData != null) + { + //Query String and form data can override variable path matches + //path variables < query string < form data + foreach (var name in queryStringAndFormData) + { + requestKeyValuesMap[name.Key] = name.Value; + } + } + + return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap); + } + + public override int GetHashCode() + { + return UniqueMatchHashKey.GetHashCode(); + } + } +}
\ No newline at end of file diff --git a/ServiceStack/Host/ServiceController.cs b/ServiceStack/Host/ServiceController.cs new file mode 100644 index 000000000..703f06365 --- /dev/null +++ b/ServiceStack/Host/ServiceController.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request); + public delegate object ActionInvokerFn(object intance, object request); + public delegate void VoidActionInvokerFn(object intance, object request); + + public class ServiceController + { + private readonly Func<IEnumerable<Type>> _resolveServicesFn; + + public ServiceController(Func<IEnumerable<Type>> resolveServicesFn) + { + _resolveServicesFn = resolveServicesFn; + this.RequestTypeFactoryMap = new Dictionary<Type, Func<IRequest, object>>(); + } + + public Dictionary<Type, Func<IRequest, object>> RequestTypeFactoryMap { get; set; } + + public void Init() + { + foreach (var serviceType in _resolveServicesFn()) + { + RegisterService(serviceType); + } + } + + private Type[] GetGenericArguments(Type type) + { + return type.GetTypeInfo().IsGenericTypeDefinition + ? type.GetTypeInfo().GenericTypeParameters + : type.GetTypeInfo().GenericTypeArguments; + } + + public void RegisterService(Type serviceType) + { + var processedReqs = new HashSet<Type>(); + + var actions = ServiceExecGeneral.Reset(serviceType); + + var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo(); + + var appHost = ServiceStackHost.Instance; + foreach (var mi in serviceType.GetActions()) + { + var requestType = mi.GetParameters()[0].ParameterType; + if (processedReqs.Contains(requestType)) continue; + processedReqs.Add(requestType); + + ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); + + var returnMarker = requestType.GetTypeWithGenericTypeDefinitionOf(typeof(IReturn<>)); + var responseType = returnMarker != null ? + GetGenericArguments(returnMarker)[0] + : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ? + mi.ReturnType + : Type.GetType(requestType.FullName + "Response"); + + RegisterRestPaths(requestType); + + appHost.Metadata.Add(serviceType, requestType, responseType); + + if (requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo())) + { + this.RequestTypeFactoryMap[requestType] = req => + { + var restPath = req.GetRoute(); + var request = RestHandler.CreateRequest(req, restPath, req.GetRequestParams(), ServiceStackHost.Instance.CreateInstance(requestType)); + + var rawReq = (IRequiresRequestStream)request; + rawReq.RequestStream = req.InputStream; + return rawReq; + }; + } + } + } + + public readonly Dictionary<string, List<RestPath>> RestPathMap = new Dictionary<string, List<RestPath>>(); + + public void RegisterRestPaths(Type requestType) + { + var appHost = ServiceStackHost.Instance; + var attrs = appHost.GetRouteAttributes(requestType); + foreach (MediaBrowser.Model.Services.RouteAttribute attr in attrs) + { + var restPath = new RestPath(requestType, attr.Path, attr.Verbs, attr.Summary, attr.Notes); + + if (!restPath.IsValid) + throw new NotSupportedException(string.Format( + "RestPath '{0}' on Type '{1}' is not Valid", attr.Path, requestType.GetOperationName())); + + RegisterRestPath(restPath); + } + } + + private static readonly char[] InvalidRouteChars = new[] { '?', '&' }; + + public void RegisterRestPath(RestPath restPath) + { + if (!restPath.Path.StartsWith("/")) + throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetOperationName())); + if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) + throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. " + + "See https://github.com/ServiceStack/ServiceStack/wiki/Routing for info on valid routes.", restPath.Path, restPath.RequestType.GetOperationName())); + + List<RestPath> pathsAtFirstMatch; + if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out pathsAtFirstMatch)) + { + pathsAtFirstMatch = new List<RestPath>(); + RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch; + } + pathsAtFirstMatch.Add(restPath); + } + + public void AfterInit() + { + var appHost = ServiceStackHost.Instance; + + //Register any routes configured on Metadata.Routes + foreach (var restPath in appHost.RestPaths) + { + RegisterRestPath(restPath); + } + + //Sync the RestPaths collections + appHost.RestPaths.Clear(); + appHost.RestPaths.AddRange(RestPathMap.Values.SelectMany(x => x)); + } + + public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) + { + var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo); + + List<RestPath> firstMatches; + + var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts); + foreach (var potentialHashMatch in yieldedHashMatches) + { + if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue; + + var bestScore = -1; + foreach (var restPath in firstMatches) + { + var score = restPath.MatchScore(httpMethod, matchUsingPathParts); + if (score > bestScore) bestScore = score; + } + if (bestScore > 0) + { + foreach (var restPath in firstMatches) + { + if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts)) + return restPath; + } + } + } + + var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts); + foreach (var potentialHashMatch in yieldedWildcardMatches) + { + if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue; + + var bestScore = -1; + foreach (var restPath in firstMatches) + { + var score = restPath.MatchScore(httpMethod, matchUsingPathParts); + if (score > bestScore) bestScore = score; + } + if (bestScore > 0) + { + foreach (var restPath in firstMatches) + { + if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts)) + return restPath; + } + } + } + + return null; + } + + public async Task<object> Execute(object requestDto, IRequest req) + { + req.Dto = requestDto; + var requestType = requestDto.GetType(); + req.OperationName = requestType.Name; + + var serviceType = ServiceStackHost.Instance.Metadata.GetServiceTypeByRequest(requestType); + + var service = ServiceStackHost.Instance.CreateInstance(serviceType); + + //var service = typeFactory.CreateInstance(serviceType); + + var serviceRequiresContext = service as IRequiresRequest; + if (serviceRequiresContext != null) + { + serviceRequiresContext.Request = req; + } + + if (req.Dto == null) // Don't override existing batched DTO[] + req.Dto = requestDto; + + //Executes the service and returns the result + var response = await ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetOperationName()).ConfigureAwait(false); + + if (req.Response.Dto == null) + req.Response.Dto = response; + + return response; + } + } + +}
\ No newline at end of file diff --git a/ServiceStack/Host/ServiceExec.cs b/ServiceStack/Host/ServiceExec.cs new file mode 100644 index 000000000..cb501a3ad --- /dev/null +++ b/ServiceStack/Host/ServiceExec.cs @@ -0,0 +1,156 @@ +//Copyright (c) Service Stack LLC. All Rights Reserved. +//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public static class ServiceExecExtensions + { + public static IEnumerable<MethodInfo> GetActions(this Type serviceType) + { + foreach (var mi in serviceType.GetRuntimeMethods().Where(i => i.IsPublic && !i.IsStatic)) + { + if (mi.GetParameters().Length != 1) + continue; + + var actionName = mi.Name.ToUpper(); + if (!HttpMethods.AllVerbs.Contains(actionName) && actionName != ActionContext.AnyAction) + continue; + + yield return mi; + } + } + } + + internal static class ServiceExecGeneral + { + public static Dictionary<string, ActionContext> execMap = new Dictionary<string, ActionContext>(); + + public static void CreateServiceRunnersFor(Type requestType, List<ActionContext> actions) + { + foreach (var actionCtx in actions) + { + if (execMap.ContainsKey(actionCtx.Id)) continue; + + execMap[actionCtx.Id] = actionCtx; + } + } + + public static async Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) + { + var actionName = request.Verb + ?? HttpMethods.Post; //MQ Services + + ActionContext actionContext; + if (ServiceExecGeneral.execMap.TryGetValue(ActionContext.Key(serviceType, actionName, requestName), out actionContext) + || ServiceExecGeneral.execMap.TryGetValue(ActionContext.AnyKey(serviceType, requestName), out actionContext)) + { + if (actionContext.RequestFilters != null) + { + foreach (var requestFilter in actionContext.RequestFilters) + { + requestFilter.RequestFilter(request, request.Response, requestDto); + if (request.Response.IsClosed) return null; + } + } + + var response = actionContext.ServiceAction(instance, requestDto); + + var taskResponse = response as Task; + if (taskResponse != null) + { + await taskResponse.ConfigureAwait(false); + response = ServiceStackHost.Instance.GetTaskResult(taskResponse, requestName); + } + + return response; + } + + var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLower(); + throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetOperationName(), expectedMethodName, serviceType.GetOperationName())); + } + + public static List<ActionContext> Reset(Type serviceType) + { + var actions = new List<ActionContext>(); + + foreach (var mi in serviceType.GetActions()) + { + var actionName = mi.Name.ToUpper(); + var args = mi.GetParameters(); + + var requestType = args[0].ParameterType; + var actionCtx = new ActionContext + { + Id = ActionContext.Key(serviceType, actionName, requestType.GetOperationName()) + }; + + try + { + actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); + } + catch + { + //Potential problems with MONO, using reflection for fallback + actionCtx.ServiceAction = (service, request) => + mi.Invoke(service, new[] { request }); + } + + var reqFilters = new List<IHasRequestFilter>(); + + foreach (var attr in mi.GetCustomAttributes(true)) + { + var hasReqFilter = attr as IHasRequestFilter; + + if (hasReqFilter != null) + reqFilters.Add(hasReqFilter); + } + + if (reqFilters.Count > 0) + actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); + + actions.Add(actionCtx); + } + + return actions; + } + + private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi) + { + var serviceParam = Expression.Parameter(typeof(object), "serviceObj"); + var serviceStrong = Expression.Convert(serviceParam, serviceType); + + var requestDtoParam = Expression.Parameter(typeof(object), "requestDto"); + var requestDtoStrong = Expression.Convert(requestDtoParam, requestType); + + Expression callExecute = Expression.Call( + serviceStrong, mi, requestDtoStrong); + + if (mi.ReturnType != typeof(void)) + { + var executeFunc = Expression.Lambda<ActionInvokerFn> + (callExecute, serviceParam, requestDtoParam).Compile(); + + return executeFunc; + } + else + { + var executeFunc = Expression.Lambda<VoidActionInvokerFn> + (callExecute, serviceParam, requestDtoParam).Compile(); + + return (service, request) => + { + executeFunc(service, request); + return null; + }; + } + } + } +}
\ No newline at end of file diff --git a/ServiceStack/Host/ServiceMetadata.cs b/ServiceStack/Host/ServiceMetadata.cs new file mode 100644 index 000000000..240e6f32d --- /dev/null +++ b/ServiceStack/Host/ServiceMetadata.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace ServiceStack.Host +{ + public class ServiceMetadata + { + public ServiceMetadata() + { + this.OperationsMap = new Dictionary<Type, Type>(); + } + + public Dictionary<Type, Type> OperationsMap { get; protected set; } + + public void Add(Type serviceType, Type requestType, Type responseType) + { + this.OperationsMap[requestType] = serviceType; + } + + public Type GetServiceTypeByRequest(Type requestType) + { + Type serviceType; + OperationsMap.TryGetValue(requestType, out serviceType); + return serviceType; + } + } +} diff --git a/ServiceStack/HttpHandlerFactory.cs b/ServiceStack/HttpHandlerFactory.cs new file mode 100644 index 000000000..d48bfeb5f --- /dev/null +++ b/ServiceStack/HttpHandlerFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public class HttpHandlerFactory + { + // Entry point for HttpListener + public static RestHandler GetHandler(IHttpRequest httpReq) + { + var pathInfo = httpReq.PathInfo; + + var pathParts = pathInfo.TrimStart('/').Split('/'); + if (pathParts.Length == 0) return null; + + string contentType; + var restPath = RestHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out contentType); + if (restPath != null) + return new RestHandler { RestPath = restPath, RequestName = restPath.RequestType.GetOperationName(), ResponseContentType = contentType }; + + return null; + } + } +}
\ No newline at end of file diff --git a/ServiceStack/HttpRequestExtensions.cs b/ServiceStack/HttpRequestExtensions.cs new file mode 100644 index 000000000..c34d62601 --- /dev/null +++ b/ServiceStack/HttpRequestExtensions.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public static class HttpRequestExtensions + { + /** + * + Input: http://localhost:96/Cambia3/Temp/Test.aspx/path/info?q=item#fragment + + Some HttpRequest path and URL properties: + Request.ApplicationPath: /Cambia3 + Request.CurrentExecutionFilePath: /Cambia3/Temp/Test.aspx + Request.FilePath: /Cambia3/Temp/Test.aspx + Request.Path: /Cambia3/Temp/Test.aspx/path/info + Request.PathInfo: /path/info + Request.PhysicalApplicationPath: D:\Inetpub\wwwroot\CambiaWeb\Cambia3\ + Request.QueryString: /Cambia3/Temp/Test.aspx/path/info?query=arg + Request.Url.AbsolutePath: /Cambia3/Temp/Test.aspx/path/info + Request.Url.AbsoluteUri: http://localhost:96/Cambia3/Temp/Test.aspx/path/info?query=arg + Request.Url.Fragment: + Request.Url.Host: localhost + Request.Url.LocalPath: /Cambia3/Temp/Test.aspx/path/info + Request.Url.PathAndQuery: /Cambia3/Temp/Test.aspx/path/info?query=arg + Request.Url.Port: 96 + Request.Url.Query: ?query=arg + Request.Url.Scheme: http + Request.Url.Segments: / + Cambia3/ + Temp/ + Test.aspx/ + path/ + info + * */ + + /// <summary> + /// Duplicate Params are given a unique key by appending a #1 suffix + /// </summary> + public static Dictionary<string, string> GetRequestParams(this IRequest request) + { + var map = new Dictionary<string, string>(); + + foreach (var name in request.QueryString.Keys) + { + if (name == null) continue; //thank you ASP.NET + + var values = request.QueryString.GetValues(name); + if (values.Length == 1) + { + map[name] = values[0]; + } + else + { + for (var i = 0; i < values.Length; i++) + { + map[name + (i == 0 ? "" : "#" + i)] = values[i]; + } + } + } + + if ((request.Verb == HttpMethods.Post || request.Verb == HttpMethods.Put) + && request.FormData != null) + { + foreach (var name in request.FormData.Keys) + { + if (name == null) continue; //thank you ASP.NET + + var values = request.FormData.GetValues(name); + if (values.Length == 1) + { + map[name] = values[0]; + } + else + { + for (var i = 0; i < values.Length; i++) + { + map[name + (i == 0 ? "" : "#" + i)] = values[i]; + } + } + } + } + + return map; + } + + /// <summary> + /// Duplicate params have their values joined together in a comma-delimited string + /// </summary> + public static Dictionary<string, string> GetFlattenedRequestParams(this IRequest request) + { + var map = new Dictionary<string, string>(); + + foreach (var name in request.QueryString.Keys) + { + if (name == null) continue; //thank you ASP.NET + map[name] = request.QueryString[name]; + } + + if ((request.Verb == HttpMethods.Post || request.Verb == HttpMethods.Put) + && request.FormData != null) + { + foreach (var name in request.FormData.Keys) + { + if (name == null) continue; //thank you ASP.NET + map[name] = request.FormData[name]; + } + } + + return map; + } + + public static void SetRoute(this IRequest req, RestPath route) + { + req.Items["__route"] = route; + } + + public static RestPath GetRoute(this IRequest req) + { + object route; + req.Items.TryGetValue("__route", out route); + return route as RestPath; + } + } +}
\ No newline at end of file diff --git a/ServiceStack/HttpResponseExtensionsInternal.cs b/ServiceStack/HttpResponseExtensionsInternal.cs new file mode 100644 index 000000000..1195f63ca --- /dev/null +++ b/ServiceStack/HttpResponseExtensionsInternal.cs @@ -0,0 +1,237 @@ +//Copyright (c) Service Stack LLC. All Rights Reserved. +//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public static class HttpResponseExtensionsInternal + { + public static async Task<bool> WriteToOutputStream(IResponse response, object result) + { + var asyncStreamWriter = result as IAsyncStreamWriter; + if (asyncStreamWriter != null) + { + await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false); + return true; + } + + var streamWriter = result as IStreamWriter; + if (streamWriter != null) + { + streamWriter.WriteTo(response.OutputStream); + return true; + } + + var stream = result as Stream; + if (stream != null) + { + WriteTo(stream, response.OutputStream); + return true; + } + + var bytes = result as byte[]; + if (bytes != null) + { + response.ContentType = "application/octet-stream"; + response.SetContentLength(bytes.Length); + + response.OutputStream.Write(bytes, 0, bytes.Length); + return true; + } + + return false; + } + + public static long WriteTo(Stream inStream, Stream outStream) + { + var memoryStream = inStream as MemoryStream; + if (memoryStream != null) + { + memoryStream.WriteTo(outStream); + return memoryStream.Position; + } + + var data = new byte[4096]; + long total = 0; + int bytesRead; + + while ((bytesRead = inStream.Read(data, 0, data.Length)) > 0) + { + outStream.Write(data, 0, bytesRead); + total += bytesRead; + } + + return total; + } + + /// <summary> + /// End a ServiceStack Request with no content + /// </summary> + public static void EndRequestWithNoContent(this IResponse httpRes) + { + if (httpRes.StatusCode == (int)HttpStatusCode.OK) + { + httpRes.StatusCode = (int)HttpStatusCode.NoContent; + } + + httpRes.SetContentLength(0); + } + + public static Task WriteToResponse(this IResponse httpRes, MediaBrowser.Model.Services.IRequest httpReq, object result) + { + if (result == null) + { + httpRes.EndRequestWithNoContent(); + return Task.FromResult(true); + } + + var httpResult = result as IHttpResult; + if (httpResult != null) + { + httpResult.RequestContext = httpReq; + httpReq.ResponseContentType = httpResult.ContentType ?? httpReq.ResponseContentType; + var httpResSerializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType); + return httpRes.WriteToResponse(httpResult, httpResSerializer, httpReq); + } + + var serializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType); + return httpRes.WriteToResponse(result, serializer, httpReq); + } + + private static object GetDto(object response) + { + if (response == null) return null; + var httpResult = response as IHttpResult; + return httpResult != null ? httpResult.Response : response; + } + + /// <summary> + /// Writes to response. + /// Response headers are customizable by implementing IHasHeaders an returning Dictionary of Http headers. + /// </summary> + /// <param name="response">The response.</param> + /// <param name="result">Whether or not it was implicity handled by ServiceStack's built-in handlers.</param> + /// <param name="defaultAction">The default action.</param> + /// <param name="request">The serialization context.</param> + /// <returns></returns> + public static async Task WriteToResponse(this IResponse response, object result, Action<IRequest, object, IResponse> defaultAction, MediaBrowser.Model.Services.IRequest request) + { + var defaultContentType = request.ResponseContentType; + if (result == null) + { + response.EndRequestWithNoContent(); + return; + } + + var httpResult = result as IHttpResult; + if (httpResult != null) + { + if (httpResult.RequestContext == null) + httpResult.RequestContext = request; + + response.Dto = response.Dto ?? GetDto(httpResult); + + response.StatusCode = httpResult.Status; + response.StatusDescription = httpResult.StatusDescription ?? httpResult.StatusCode.ToString(); + if (string.IsNullOrEmpty(httpResult.ContentType)) + { + httpResult.ContentType = defaultContentType; + } + response.ContentType = httpResult.ContentType; + + if (httpResult.Cookies != null) + { + var httpRes = response as IHttpResponse; + if (httpRes != null) + { + foreach (var cookie in httpResult.Cookies) + { + httpRes.SetCookie(cookie); + } + } + } + } + else + { + response.Dto = result; + } + + /* Mono Error: Exception: Method not found: 'System.Web.HttpResponse.get_Headers' */ + var responseOptions = result as IHasHeaders; + if (responseOptions != null) + { + //Reserving options with keys in the format 'xx.xxx' (No Http headers contain a '.' so its a safe restriction) + const string reservedOptions = "."; + + foreach (var responseHeaders in responseOptions.Headers) + { + if (responseHeaders.Key.Contains(reservedOptions)) continue; + if (responseHeaders.Key == "Content-Length") + { + response.SetContentLength(long.Parse(responseHeaders.Value)); + continue; + } + + response.AddHeader(responseHeaders.Key, responseHeaders.Value); + } + } + + //ContentType='text/html' is the default for a HttpResponse + //Do not override if another has been set + if (response.ContentType == null || response.ContentType == "text/html") + { + response.ContentType = defaultContentType; + } + + if (new HashSet<string> { "application/json", }.Contains(response.ContentType)) + { + response.ContentType += "; charset=utf-8"; + } + + var disposableResult = result as IDisposable; + var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false); + if (writeToOutputStreamResult) + { + response.Flush(); //required for Compression + if (disposableResult != null) disposableResult.Dispose(); + return; + } + + if (httpResult != null) + result = httpResult.Response; + + var responseText = result as string; + if (responseText != null) + { + if (response.ContentType == null || response.ContentType == "text/html") + response.ContentType = defaultContentType; + response.Write(responseText); + + return; + } + + if (defaultAction == null) + { + throw new ArgumentNullException("defaultAction", String.Format( + "As result '{0}' is not a supported responseType, a defaultAction must be supplied", + (result != null ? result.GetType().GetOperationName() : ""))); + } + + + if (result != null) + defaultAction(request, result, response); + + if (disposableResult != null) + disposableResult.Dispose(); + } + + } +} diff --git a/ServiceStack/HttpResult.cs b/ServiceStack/HttpResult.cs new file mode 100644 index 000000000..23a5cdffb --- /dev/null +++ b/ServiceStack/HttpResult.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public class HttpResult + : IHttpResult, IAsyncStreamWriter + { + public HttpResult() + : this((object)null, null) + { + } + + public HttpResult(object response) + : this(response, null) + { + } + + public HttpResult(object response, string contentType) + : this(response, contentType, HttpStatusCode.OK) + { + } + + public HttpResult(HttpStatusCode statusCode, string statusDescription) + : this() + { + StatusCode = statusCode; + StatusDescription = statusDescription; + } + + public HttpResult(object response, HttpStatusCode statusCode) + : this(response, null, statusCode) + { } + + public HttpResult(object response, string contentType, HttpStatusCode statusCode) + { + this.Headers = new Dictionary<string, string>(); + this.Cookies = new List<Cookie>(); + + this.Response = response; + this.ContentType = contentType; + this.StatusCode = statusCode; + } + + public HttpResult(Stream responseStream, string contentType) + : this(null, contentType, HttpStatusCode.OK) + { + this.ResponseStream = responseStream; + } + + public HttpResult(string responseText, string contentType) + : this(null, contentType, HttpStatusCode.OK) + { + this.ResponseText = responseText; + } + + public HttpResult(byte[] responseBytes, string contentType) + : this(null, contentType, HttpStatusCode.OK) + { + this.ResponseStream = new MemoryStream(responseBytes); + } + + public string ResponseText { get; private set; } + + public Stream ResponseStream { get; private set; } + + public string ContentType { get; set; } + + public IDictionary<string, string> Headers { get; private set; } + + public List<Cookie> Cookies { get; private set; } + + public string ETag { get; set; } + + public TimeSpan? Age { get; set; } + + public TimeSpan? MaxAge { get; set; } + + public DateTime? Expires { get; set; } + + public DateTime? LastModified { get; set; } + + public Func<IDisposable> ResultScope { get; set; } + + public string Location + { + set + { + if (StatusCode == HttpStatusCode.OK) + StatusCode = HttpStatusCode.Redirect; + + this.Headers["Location"] = value; + } + } + + public void SetPermanentCookie(string name, string value) + { + SetCookie(name, value, DateTime.UtcNow.AddYears(20), null); + } + + public void SetPermanentCookie(string name, string value, string path) + { + SetCookie(name, value, DateTime.UtcNow.AddYears(20), path); + } + + public void SetSessionCookie(string name, string value) + { + SetSessionCookie(name, value, null); + } + + public void SetSessionCookie(string name, string value, string path) + { + path = path ?? "/"; + this.Headers["Set-Cookie"] = string.Format("{0}={1};path=" + path, name, value); + } + + public void SetCookie(string name, string value, TimeSpan expiresIn, string path) + { + var expiresAt = DateTime.UtcNow.Add(expiresIn); + SetCookie(name, value, expiresAt, path); + } + + public void SetCookie(string name, string value, DateTime expiresAt, string path, bool secure = false, bool httpOnly = false) + { + path = path ?? "/"; + var cookie = string.Format("{0}={1};expires={2};path={3}", name, value, expiresAt.ToString("R"), path); + if (secure) + cookie += ";Secure"; + if (httpOnly) + cookie += ";HttpOnly"; + + this.Headers["Set-Cookie"] = cookie; + } + + public void DeleteCookie(string name) + { + var cookie = string.Format("{0}=;expires={1};path=/", name, DateTime.UtcNow.AddDays(-1).ToString("R")); + this.Headers["Set-Cookie"] = cookie; + } + + public int Status { get; set; } + + public HttpStatusCode StatusCode + { + get { return (HttpStatusCode)Status; } + set { Status = (int)value; } + } + + public string StatusDescription { get; set; } + + public object Response { get; set; } + + public MediaBrowser.Model.Services.IRequest RequestContext { get; set; } + + public string View { get; set; } + + public string Template { get; set; } + + public int PaddingLength { get; set; } + + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) + { + try + { + await WriteToInternalAsync(responseStream, cancellationToken).ConfigureAwait(false); + responseStream.Flush(); + } + finally + { + DisposeStream(); + } + } + + public static Task WriteTo(Stream inStream, Stream outStream, CancellationToken cancellationToken) + { + var memoryStream = inStream as MemoryStream; + if (memoryStream != null) + { + memoryStream.WriteTo(outStream); + return Task.FromResult(true); + } + + return inStream.CopyToAsync(outStream, 81920, cancellationToken); + } + + public async Task WriteToInternalAsync(Stream responseStream, CancellationToken cancellationToken) + { + var response = RequestContext != null ? RequestContext.Response : null; + + if (this.ResponseStream != null) + { + if (response != null) + { + var ms = ResponseStream as MemoryStream; + if (ms != null) + { + response.SetContentLength(ms.Length); + + await ms.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false); + return; + } + } + + await WriteTo(this.ResponseStream, responseStream, cancellationToken).ConfigureAwait(false); + return; + } + + if (this.ResponseText != null) + { + var bytes = Encoding.UTF8.GetBytes(this.ResponseText); + if (response != null) + response.SetContentLength(bytes.Length); + + await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + return; + } + + var bytesResponse = this.Response as byte[]; + if (bytesResponse != null) + { + if (response != null) + response.SetContentLength(bytesResponse.Length); + + await responseStream.WriteAsync(bytesResponse, 0, bytesResponse.Length).ConfigureAwait(false); + return; + } + + ContentTypes.Instance.SerializeToStream(this.RequestContext, this.Response, responseStream); + } + + private void DisposeStream() + { + try + { + if (ResponseStream != null) + { + this.ResponseStream.Dispose(); + } + } + catch { /*ignore*/ } + } + } +} diff --git a/ServiceStack/HttpUtils.cs b/ServiceStack/HttpUtils.cs new file mode 100644 index 000000000..41d191d61 --- /dev/null +++ b/ServiceStack/HttpUtils.cs @@ -0,0 +1,34 @@ +//Copyright (c) Service Stack LLC. All Rights Reserved. +//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + +using System; +using System.Collections.Generic; + +namespace ServiceStack +{ + internal static class HttpMethods + { + static readonly string[] allVerbs = new[] { + "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 + "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 + "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", + "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 + "ORDERPATCH", // RFC 3648 + "ACL", // RFC 3744 + "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ + "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ + "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", + "POLL", "SUBSCRIBE", "UNSUBSCRIBE" //MS Exchange WebDav: http://msdn.microsoft.com/en-us/library/aa142917.aspx + }; + + public static HashSet<string> AllVerbs = new HashSet<string>(allVerbs); + + public const string Get = "GET"; + public const string Put = "PUT"; + public const string Post = "POST"; + public const string Delete = "DELETE"; + public const string Options = "OPTIONS"; + public const string Head = "HEAD"; + public const string Patch = "PATCH"; + } +} diff --git a/ServiceStack/Properties/AssemblyInfo.cs b/ServiceStack/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6073dc0b4 --- /dev/null +++ b/ServiceStack/Properties/AssemblyInfo.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ServiceStack")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Service Stack LLC")] +[assembly: AssemblyProduct("ServiceStack")] +[assembly: AssemblyCopyright("Copyright (c) ServiceStack 2016")] +[assembly: AssemblyTrademark("Service Stack")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("06704d66-af8e-411f-8260-8d05de5ce6ad")] + +[assembly: AssemblyVersion("4.0.0.0")] +[assembly: AssemblyFileVersion("4.0.0.0")] diff --git a/ServiceStack/ReflectionExtensions.cs b/ServiceStack/ReflectionExtensions.cs new file mode 100644 index 000000000..bbabd0dd7 --- /dev/null +++ b/ServiceStack/ReflectionExtensions.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace ServiceStack +{ + public static class ReflectionExtensions + { + public static bool IsInstanceOf(this Type type, Type thisOrBaseType) + { + while (type != null) + { + if (type == thisOrBaseType) + return true; + + type = type.BaseType(); + } + return false; + } + + public static Type FirstGenericType(this Type type) + { + while (type != null) + { + if (type.IsGeneric()) + return type; + + type = type.BaseType(); + } + return null; + } + + public static Type GetTypeWithGenericTypeDefinitionOf(this Type type, Type genericTypeDefinition) + { + foreach (var t in type.GetTypeInterfaces()) + { + if (t.IsGeneric() && t.GetGenericTypeDefinition() == genericTypeDefinition) + { + return t; + } + } + + var genericType = type.FirstGenericType(); + if (genericType != null && genericType.GetGenericTypeDefinition() == genericTypeDefinition) + { + return genericType; + } + + return null; + } + + public static PropertyInfo[] GetAllProperties(this Type type) + { + if (type.IsInterface()) + { + var propertyInfos = new List<PropertyInfo>(); + + var considered = new List<Type>(); + var queue = new Queue<Type>(); + considered.Add(type); + queue.Enqueue(type); + + while (queue.Count > 0) + { + var subType = queue.Dequeue(); + foreach (var subInterface in subType.GetTypeInterfaces()) + { + if (considered.Contains(subInterface)) continue; + + considered.Add(subInterface); + queue.Enqueue(subInterface); + } + + var typeProperties = subType.GetTypesProperties(); + + var newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); + } + + return propertyInfos.ToArray(); + } + + return type.GetTypesProperties() + .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties + .ToArray(); + } + + public static PropertyInfo[] GetPublicProperties(this Type type) + { + if (type.IsInterface()) + { + var propertyInfos = new List<PropertyInfo>(); + + var considered = new List<Type>(); + var queue = new Queue<Type>(); + considered.Add(type); + queue.Enqueue(type); + + while (queue.Count > 0) + { + var subType = queue.Dequeue(); + foreach (var subInterface in subType.GetTypeInterfaces()) + { + if (considered.Contains(subInterface)) continue; + + considered.Add(subInterface); + queue.Enqueue(subInterface); + } + + var typeProperties = subType.GetTypesPublicProperties(); + + var newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); + } + + return propertyInfos.ToArray(); + } + + return type.GetTypesPublicProperties() + .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties + .ToArray(); + } + + public const string DataMember = "DataMemberAttribute"; + + internal static string[] IgnoreAttributesNamed = new[] { + "IgnoreDataMemberAttribute", + "JsonIgnoreAttribute" + }; + + public static PropertyInfo[] GetSerializableProperties(this Type type) + { + var properties = type.IsDto() + ? type.GetAllProperties() + : type.GetPublicProperties(); + return properties.OnlySerializableProperties(type); + } + + + private static List<Type> _excludeTypes = new List<Type> { typeof(Stream) }; + + public static PropertyInfo[] OnlySerializableProperties(this PropertyInfo[] properties, Type type = null) + { + var isDto = type.IsDto(); + var readableProperties = properties.Where(x => x.PropertyGetMethod(nonPublic: isDto) != null); + + if (isDto) + { + return readableProperties.Where(attr => + attr.HasAttribute<DataMemberAttribute>()).ToArray(); + } + + // else return those properties that are not decorated with IgnoreDataMember + return readableProperties + .Where(prop => prop.AllAttributes() + .All(attr => + { + var name = attr.GetType().Name; + return !IgnoreAttributesNamed.Contains(name); + })) + .Where(prop => !_excludeTypes.Contains(prop.PropertyType)) + .ToArray(); + } + } + + public static class PlatformExtensions //Because WinRT is a POS + { + public static bool IsInterface(this Type type) + { + return type.GetTypeInfo().IsInterface; + } + + public static bool IsGeneric(this Type type) + { + return type.GetTypeInfo().IsGenericType; + } + + public static Type BaseType(this Type type) + { + return type.GetTypeInfo().BaseType; + } + + public static Type[] GetTypeInterfaces(this Type type) + { + return type.GetTypeInfo().ImplementedInterfaces.ToArray(); + } + + internal static PropertyInfo[] GetTypesPublicProperties(this Type subType) + { + var pis = new List<PropertyInfo>(); + foreach (var pi in subType.GetRuntimeProperties()) + { + var mi = pi.GetMethod ?? pi.SetMethod; + if (mi != null && mi.IsStatic) continue; + pis.Add(pi); + } + return pis.ToArray(); + } + + internal static PropertyInfo[] GetTypesProperties(this Type subType) + { + var pis = new List<PropertyInfo>(); + foreach (var pi in subType.GetRuntimeProperties()) + { + var mi = pi.GetMethod ?? pi.SetMethod; + if (mi != null && mi.IsStatic) continue; + pis.Add(pi); + } + return pis.ToArray(); + } + + public static bool HasAttribute<T>(this Type type) + { + return type.AllAttributes().Any(x => x.GetType() == typeof(T)); + } + + public static bool HasAttribute<T>(this PropertyInfo pi) + { + return pi.AllAttributes().Any(x => x.GetType() == typeof(T)); + } + + public static bool IsDto(this Type type) + { + if (type == null) + return false; + + return type.HasAttribute<DataContractAttribute>(); + } + + public static MethodInfo PropertyGetMethod(this PropertyInfo pi, bool nonPublic = false) + { + return pi.GetMethod; + } + + public static object[] AllAttributes(this PropertyInfo propertyInfo) + { + return propertyInfo.GetCustomAttributes(true).ToArray(); + } + + public static object[] AllAttributes(this PropertyInfo propertyInfo, Type attrType) + { + return propertyInfo.GetCustomAttributes(true).Where(x => attrType.IsInstanceOf(x.GetType())).ToArray(); + } + + public static object[] AllAttributes(this Type type) + { + return type.GetTypeInfo().GetCustomAttributes(true).ToArray(); + } + + public static TAttr[] AllAttributes<TAttr>(this PropertyInfo pi) + { + return pi.AllAttributes(typeof(TAttr)).Cast<TAttr>().ToArray(); + } + + public static TAttr[] AllAttributes<TAttr>(this Type type) + where TAttr : Attribute + { + return type.GetTypeInfo().GetCustomAttributes<TAttr>(true).ToArray(); + } + } +} diff --git a/ServiceStack/ServiceStack.csproj b/ServiceStack/ServiceStack.csproj new file mode 100644 index 000000000..3402339a6 --- /dev/null +++ b/ServiceStack/ServiceStack.csproj @@ -0,0 +1,131 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0"> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProductVersion>9.0.30729</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{680A1709-25EB-4D52-A87F-EE03FFD94BAA}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>ServiceStack</RootNamespace> + <AssemblyName>ServiceStack</AssemblyName> + <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> + <TargetFrameworkProfile>Profile7</TargetFrameworkProfile> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + <FileUpgradeFlags> + </FileUpgradeFlags> + <OldToolsVersion>3.5</OldToolsVersion> + <UpgradeBackupLocation /> + <PublishUrl>publish\</PublishUrl> + <Install>true</Install> + <InstallFrom>Disk</InstallFrom> + <UpdateEnabled>false</UpdateEnabled> + <UpdateMode>Foreground</UpdateMode> + <UpdateInterval>7</UpdateInterval> + <UpdateIntervalUnits>Days</UpdateIntervalUnits> + <UpdatePeriodically>false</UpdatePeriodically> + <UpdateRequired>false</UpdateRequired> + <MapFileExtensions>true</MapFileExtensions> + <ApplicationRevision>0</ApplicationRevision> + <ApplicationVersion>1.0.0.%2a</ApplicationVersion> + <IsWebBootstrapper>false</IsWebBootstrapper> + <UseApplicationTrust>false</UseApplicationTrust> + <BootstrapperEnabled>true</BootstrapperEnabled> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>True</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>False</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>TRACE;DEBUG;MONO</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>True</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> + <DocumentationFile> + </DocumentationFile> + <Prefer32Bit>false</Prefer32Bit> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Signed|AnyCPU'"> + <OutputPath>bin\Signed\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <DocumentationFile>bin\Release\ServiceStack.XML</DocumentationFile> + <Optimize>true</Optimize> + <DebugType>pdbonly</DebugType> + <PlatformTarget>AnyCPU</PlatformTarget> + <ErrorReport>prompt</ErrorReport> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> + <Prefer32Bit>false</Prefer32Bit> + </PropertyGroup> + <ItemGroup> + <Compile Include="HttpUtils.cs" /> + <Compile Include="Host\ContentTypes.cs" /> + <Compile Include="ReflectionExtensions.cs" /> + <Compile Include="StringMapTypeDeserializer.cs" /> + <Compile Include="Host\HttpResponseStreamWrapper.cs" /> + <Compile Include="HttpResult.cs" /> + <Compile Include="ServiceStackHost.cs" /> + <Compile Include="ServiceStackHost.Runtime.cs" /> + <Compile Include="Host\ServiceExec.cs" /> + <Compile Include="UrlExtensions.cs" /> + <Compile Include="Host\ActionContext.cs" /> + <Compile Include="HttpRequestExtensions.cs" /> + <Compile Include="Host\RestPath.cs" /> + <Compile Include="Host\ServiceController.cs" /> + <Compile Include="Host\ServiceMetadata.cs" /> + <Compile Include="Host\RestHandler.cs" /> + <Compile Include="HttpResponseExtensionsInternal.cs" /> + <Compile Include="HttpHandlerFactory.cs" /> + <Compile Include="FilterAttributeCache.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="packages.config" /> + </ItemGroup> + <ItemGroup> + <BootstrapperPackage Include="Microsoft.Net.Client.3.5"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName> + <Install>false</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Windows.Installer.3.1"> + <Visible>False</Visible> + <ProductName>Windows Installer 3.1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + </ItemGroup> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> + <ItemGroup> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> + <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> + <Name>MediaBrowser.Common</Name> + </ProjectReference> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> + <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> + <Name>MediaBrowser.Model</Name> + </ProjectReference> + </ItemGroup> +</Project>
\ No newline at end of file diff --git a/ServiceStack/ServiceStack.nuget.targets b/ServiceStack/ServiceStack.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/ServiceStack/ServiceStack.nuget.targets @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8" standalone="no"?> +<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Target Name="EmitMSBuildWarning" BeforeTargets="Build"> + <Warning Text="Packages containing MSBuild targets and props files cannot be fully installed in projects targeting multiple frameworks. The MSBuild targets and props files have been ignored." /> + </Target> +</Project>
\ No newline at end of file diff --git a/ServiceStack/ServiceStack.xproj b/ServiceStack/ServiceStack.xproj new file mode 100644 index 000000000..ba8f8b8f2 --- /dev/null +++ b/ServiceStack/ServiceStack.xproj @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion> + <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> + </PropertyGroup> + <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> + <PropertyGroup Label="Globals"> + <ProjectGuid>b2d733ab-620e-4c53-88a4-4b6638ab6a7a</ProjectGuid> + <RootNamespace>ServiceStack</RootNamespace> + <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> + <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> + </PropertyGroup> + + <PropertyGroup> + <SchemaVersion>2.0</SchemaVersion> + </PropertyGroup> + <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> +</Project>
\ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.Runtime.cs b/ServiceStack/ServiceStackHost.Runtime.cs new file mode 100644 index 000000000..1a1656a0e --- /dev/null +++ b/ServiceStack/ServiceStackHost.Runtime.cs @@ -0,0 +1,74 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using MediaBrowser.Model.Services; +using ServiceStack.Support.WebHost; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost + { + /// <summary> + /// Applies the request filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// </summary> + /// <returns></returns> + public virtual bool ApplyRequestFilters(IRequest req, IResponse res, object requestDto) + { + if (res.IsClosed) return res.IsClosed; + + //Exec all RequestFilter attributes with Priority < 0 + var attributes = FilterAttributeCache.GetRequestFilterAttributes(requestDto.GetType()); + var i = 0; + for (; i < attributes.Length && attributes[i].Priority < 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + if (res.IsClosed) return res.IsClosed; + + //Exec global filters + foreach (var requestFilter in GlobalRequestFilters) + { + requestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + //Exec remaining RequestFilter attributes with Priority >= 0 + for (; i < attributes.Length && attributes[i].Priority >= 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + + /// <summary> + /// Applies the response filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// </summary> + /// <returns></returns> + public virtual bool ApplyResponseFilters(IRequest req, IResponse res, object response) + { + if (response != null) + { + if (res.IsClosed) return res.IsClosed; + } + + //Exec global filters + foreach (var responseFilter in GlobalResponseFilters) + { + responseFilter(req, res, response); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + } + +}
\ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.cs b/ServiceStack/ServiceStackHost.cs new file mode 100644 index 000000000..8a1db25e4 --- /dev/null +++ b/ServiceStack/ServiceStackHost.cs @@ -0,0 +1,104 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost : IDisposable + { + public static ServiceStackHost Instance { get; protected set; } + + protected ServiceStackHost(string serviceName) + { + ServiceName = serviceName; + ServiceController = CreateServiceController(); + + RestPaths = new List<RestPath>(); + Metadata = new ServiceMetadata(); + GlobalRequestFilters = new List<Action<IRequest, IResponse, object>>(); + GlobalResponseFilters = new List<Action<IRequest, IResponse, object>>(); + } + + public abstract void Configure(); + + public abstract object CreateInstance(Type type); + + protected abstract ServiceController CreateServiceController(); + + public virtual ServiceStackHost Init() + { + Instance = this; + + ServiceController.Init(); + Configure(); + + ServiceController.AfterInit(); + + return this; + } + + public virtual ServiceStackHost Start(string urlBase) + { + throw new NotImplementedException("Start(listeningAtUrlBase) is not supported by this AppHost"); + } + + public string ServiceName { get; set; } + + public ServiceMetadata Metadata { get; set; } + + public ServiceController ServiceController { get; set; } + + public List<RestPath> RestPaths = new List<RestPath>(); + + public List<Action<IRequest, IResponse, object>> GlobalRequestFilters { get; set; } + + public List<Action<IRequest, IResponse, object>> GlobalResponseFilters { get; set; } + + public abstract T TryResolve<T>(); + public abstract T Resolve<T>(); + + public virtual MediaBrowser.Model.Services.RouteAttribute[] GetRouteAttributes(Type requestType) + { + return requestType.AllAttributes<MediaBrowser.Model.Services.RouteAttribute>(); + } + + public abstract object GetTaskResult(Task task, string requestName); + + public abstract Func<string, object> GetParseFn(Type propertyType); + + public abstract void SerializeToJson(object o, Stream stream); + public abstract void SerializeToXml(object o, Stream stream); + public abstract object DeserializeXml(Type type, Stream stream); + public abstract object DeserializeJson(Type type, Stream stream); + + public virtual void Dispose() + { + //JsConfig.Reset(); //Clears Runtime Attributes + + Instance = null; + } + + protected abstract ILogger Logger + { + get; + } + + public void OnLogError(Type type, string message) + { + Logger.Error(message); + } + + public void OnLogError(Type type, string message, Exception ex) + { + Logger.ErrorException(message, ex); + } + } +} diff --git a/ServiceStack/StringMapTypeDeserializer.cs b/ServiceStack/StringMapTypeDeserializer.cs new file mode 100644 index 000000000..762e8aaff --- /dev/null +++ b/ServiceStack/StringMapTypeDeserializer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Linq; +using System.Reflection; + +namespace ServiceStack.Serialization +{ + /// <summary> + /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) + /// </summary> + public class StringMapTypeDeserializer + { + internal class PropertySerializerEntry + { + public PropertySerializerEntry(Action<object,object> propertySetFn, Func<string, object> propertyParseStringFn) + { + PropertySetFn = propertySetFn; + PropertyParseStringFn = propertyParseStringFn; + } + + public Action<object, object> PropertySetFn; + public Func<string,object> PropertyParseStringFn; + public Type PropertyType; + } + + private readonly Type type; + private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap + = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase); + + public Func<string, object> GetParseFn(Type propertyType) + { + //Don't JSV-decode string values for string properties + if (propertyType == typeof(string)) + return s => s; + + return ServiceStackHost.Instance.GetParseFn(propertyType); + } + + public StringMapTypeDeserializer(Type type) + { + this.type = type; + + foreach (var propertyInfo in type.GetSerializableProperties()) + { + var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo); + var propertyType = propertyInfo.PropertyType; + var propertyParseStringFn = GetParseFn(propertyType); + var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType }; + + var attr = propertyInfo.AllAttributes<DataMemberAttribute>().FirstOrDefault(); + if (attr != null && attr.Name != null) + { + propertySetterMap[attr.Name] = propertySerializer; + } + propertySetterMap[propertyInfo.Name] = propertySerializer; + } + } + + public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs) + { + string propertyName = null; + string propertyTextValue = null; + PropertySerializerEntry propertySerializerEntry = null; + + if (instance == null) + instance = ServiceStackHost.Instance.CreateInstance(type); + + foreach (var pair in keyValuePairs.Where(x => !string.IsNullOrEmpty(x.Value))) + { + propertyName = pair.Key; + propertyTextValue = pair.Value; + + if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)) + { + if (propertyName == "v") + { + continue; + } + + continue; + } + + if (propertySerializerEntry.PropertySetFn == null) + { + continue; + } + + if (propertySerializerEntry.PropertyType == typeof(bool)) + { + //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value + propertyTextValue = LeftPart(propertyTextValue, ','); + } + + var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); + if (value == null) + { + continue; + } + propertySerializerEntry.PropertySetFn(instance, value); + } + + return instance; + } + + public static string LeftPart(string strVal, char needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + } + + internal class TypeAccessor + { + public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo) + { + if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Any()) return null; + + var setMethodInfo = propertyInfo.SetMethod; + return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); + } + } +} diff --git a/ServiceStack/UrlExtensions.cs b/ServiceStack/UrlExtensions.cs new file mode 100644 index 000000000..7b5a50ef1 --- /dev/null +++ b/ServiceStack/UrlExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace ServiceStack +{ + /// <summary> + /// Donated by Ivan Korneliuk from his post: + /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html + /// + /// Modified to only allow using routes matching the supplied HTTP Verb + /// </summary> + public static class UrlExtensions + { + public static string GetOperationName(this Type type) + { + var typeName = type.FullName != null //can be null, e.g. generic types + ? LeftPart(type.FullName, "[[") //Generic Fullname + .Replace(type.Namespace + ".", "") //Trim Namespaces + .Replace("+", ".") //Convert nested into normal type + : type.Name; + + return type.IsGenericParameter ? "'" + typeName : typeName; + } + + public static string LeftPart(string strVal, string needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + } +}
\ No newline at end of file diff --git a/ServiceStack/packages.config b/ServiceStack/packages.config new file mode 100644 index 000000000..6b8deb9c9 --- /dev/null +++ b/ServiceStack/packages.config @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> +</packages>
\ No newline at end of file diff --git a/ServiceStack/project.json b/ServiceStack/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/ServiceStack/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +}
\ No newline at end of file |
